+
+
+
+
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/TestTTC.ttc b/mobile/android/fenix/app/src/androidTest/assets/resources/TestTTC.ttc
new file mode 100644
index 0000000000..a21fe89dc4
Binary files /dev/null and b/mobile/android/fenix/app/src/androidTest/assets/resources/TestTTC.ttc differ
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/TestTTF.ttf b/mobile/android/fenix/app/src/androidTest/assets/resources/TestTTF.ttf
new file mode 100644
index 0000000000..e906d012d1
Binary files /dev/null and b/mobile/android/fenix/app/src/androidTest/assets/resources/TestTTF.ttf differ
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/TestTT_-LICENSE b/mobile/android/fenix/app/src/androidTest/assets/resources/TestTT_-LICENSE
new file mode 100644
index 0000000000..67f5310de9
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/assets/resources/TestTT_-LICENSE
@@ -0,0 +1,23 @@
+From: https://raw.githubusercontent.com/fonttools/fonttools/main/LICENSE
+
+MIT License
+
+Copyright (c) 2017 Just van Rossum
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/audioSample.mp3 b/mobile/android/fenix/app/src/androidTest/assets/resources/audioSample.mp3
new file mode 100644
index 0000000000..eb0420a48b
Binary files /dev/null and b/mobile/android/fenix/app/src/androidTest/assets/resources/audioSample.mp3 differ
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/clip.mp4 b/mobile/android/fenix/app/src/androidTest/assets/resources/clip.mp4
new file mode 100644
index 0000000000..20f739c7c8
Binary files /dev/null and b/mobile/android/fenix/app/src/androidTest/assets/resources/clip.mp4 differ
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/pdfForm.pdf b/mobile/android/fenix/app/src/androidTest/assets/resources/pdfForm.pdf
new file mode 100644
index 0000000000..8c4768249e
Binary files /dev/null and b/mobile/android/fenix/app/src/androidTest/assets/resources/pdfForm.pdf differ
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/rabbit.jpg b/mobile/android/fenix/app/src/androidTest/assets/resources/rabbit.jpg
new file mode 100644
index 0000000000..3225407b1c
Binary files /dev/null and b/mobile/android/fenix/app/src/androidTest/assets/resources/rabbit.jpg differ
diff --git a/mobile/android/fenix/app/src/androidTest/assets/resources/trackingAPI.js b/mobile/android/fenix/app/src/androidTest/assets/resources/trackingAPI.js
new file mode 100644
index 0000000000..cc15eacf56
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/assets/resources/trackingAPI.js
@@ -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;
+ }
+};
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/AppRequestInterceptor.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/AppRequestInterceptor.kt
new file mode 100644
index 0000000000..a8be915ad1
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/AppRequestInterceptor.kt
@@ -0,0 +1,200 @@
+/* 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? = 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
+ 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()!!.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) +
+ "
" +
+ 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"
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/components/FontParserTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/components/FontParserTest.kt
new file mode 100644
index 0000000000..77be42c15f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/components/FontParserTest.kt
@@ -0,0 +1,79 @@
+package org.mozilla.fenix.components
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
+import org.mozilla.fenix.components.metrics.fonts.FontParser
+
+class FontParserTest {
+ @Test
+ fun testSanityAssertion() {
+ /*
+ Changing the below constant causes _all_ Nightly users to send a (large) Telemetry event containing
+ their font information. Do not change this value unless you explicitly intend this.
+ */
+ assertEquals(4, FontEnumerationWorker.kDesiredSubmissions)
+ }
+
+ @Test
+ fun testFontParsing() {
+ val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
+ val font1 = FontParser.parse("no-path", assetManager.open("resources/TestTTF.ttf"))
+ assertEquals(
+ "\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
+ "\u0000T\u0000F",
+ font1.family,
+ )
+ assertEquals(
+ "\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
+ "\u00000\u00000\u00000",
+ font1.fontVersion,
+ )
+ assertEquals(
+ "\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
+ font1.fullName,
+ )
+ assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
+ assertEquals(
+ "\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
+ "\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
+ font1.uniqueSubFamily,
+ )
+ assertEquals(
+ "C4E8CE309F44A131D061D73B2580E922A7F5ECC8D7109797AC0FF58BF8723B7B",
+ font1.hash,
+ )
+ assertEquals(3516272951, font1.created)
+ assertEquals(3573411749, font1.modified)
+ assertEquals(65536, font1.revision)
+ val font2 = FontParser.parse("no-path", assetManager.open("resources/TestTTC.ttc"))
+ assertEquals(
+ "\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
+ "\u0000T\u0000F",
+ font2.family,
+ )
+ assertEquals(
+ "\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
+ "\u00000\u00000\u00000",
+ font2.fontVersion,
+ )
+ assertEquals(
+ "\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
+ font2.fullName,
+ )
+ assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
+ assertEquals(
+ "\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
+ "\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
+ font2.uniqueSubFamily,
+ )
+ assertEquals(
+ "A8521588045ED5F1F8B07EECAAC06ED3186C644655BFAC00DD4507CD316FBDC5",
+ font2.hash,
+ )
+ assertEquals(3516272951, font2.created)
+ assertEquals(3573411749, font2.modified)
+ assertEquals(65536, font2.revision)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/components/FxaServer.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/components/FxaServer.kt
new file mode 100644
index 0000000000..2320fd50ee
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/components/FxaServer.kt
@@ -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.appservices.fxaclient.FxaConfig
+import mozilla.appservices.fxaclient.FxaServer
+
+/**
+ * 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): FxaConfig {
+ return FxaConfig(FxaServer.Stage, CLIENT_ID, REDIRECT_URL)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/customannotations/SmokeTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/customannotations/SmokeTest.kt
new file mode 100644
index 0000000000..4d8b68178a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/customannotations/SmokeTest.kt
@@ -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
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/GenericExperimentIntegrationTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/GenericExperimentIntegrationTest.kt
new file mode 100644
index 0000000000..9bd0be1044
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/GenericExperimentIntegrationTest.kt
@@ -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.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
+ }
+
+ @Test
+ 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)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile
new file mode 100644
index 0000000000..f475ca53c6
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[[source]]
+url = "https://pypi.python.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pydantic = "*"
+pytest = "*"
+pytest-html = "*"
+pytest-metadata = "*"
+pytest-variables = "*"
+pyyaml = "*"
+requests = "*"
+
+[dev-packages]
+black = "*"
+flake8 = "*"
+
+[requires]
+python_version = "3.11"
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock
new file mode 100644
index 0000000000..fb5887ad2d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock
@@ -0,0 +1,578 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "6dae5ac51aa7817578a25597da1ef783475050538443ba344c88a78969e68fd9"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.11"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.python.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "annotated-types": {
+ "hashes": [
+ "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43",
+ "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.6.0"
+ },
+ "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:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
+ "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==3.1.3"
+ },
+ "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"
+ },
+ "pydantic": {
+ "hashes": [
+ "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7",
+ "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.4.2"
+ },
+ "pydantic-core": {
+ "hashes": [
+ "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e",
+ "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33",
+ "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7",
+ "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7",
+ "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea",
+ "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4",
+ "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0",
+ "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7",
+ "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94",
+ "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff",
+ "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82",
+ "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd",
+ "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893",
+ "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e",
+ "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d",
+ "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901",
+ "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9",
+ "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c",
+ "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7",
+ "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891",
+ "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f",
+ "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a",
+ "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9",
+ "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5",
+ "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e",
+ "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a",
+ "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c",
+ "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f",
+ "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514",
+ "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b",
+ "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302",
+ "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096",
+ "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0",
+ "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27",
+ "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884",
+ "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a",
+ "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357",
+ "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430",
+ "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221",
+ "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325",
+ "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4",
+ "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05",
+ "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55",
+ "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875",
+ "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970",
+ "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc",
+ "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6",
+ "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f",
+ "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b",
+ "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d",
+ "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15",
+ "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118",
+ "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee",
+ "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e",
+ "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6",
+ "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208",
+ "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede",
+ "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3",
+ "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e",
+ "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada",
+ "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175",
+ "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a",
+ "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c",
+ "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f",
+ "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58",
+ "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f",
+ "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a",
+ "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a",
+ "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921",
+ "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e",
+ "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904",
+ "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776",
+ "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52",
+ "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf",
+ "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8",
+ "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f",
+ "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b",
+ "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63",
+ "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c",
+ "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f",
+ "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468",
+ "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e",
+ "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab",
+ "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2",
+ "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb",
+ "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb",
+ "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132",
+ "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b",
+ "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607",
+ "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934",
+ "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698",
+ "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e",
+ "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561",
+ "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de",
+ "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b",
+ "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a",
+ "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595",
+ "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402",
+ "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881",
+ "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429",
+ "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5",
+ "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7",
+ "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c",
+ "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531",
+ "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6",
+ "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.10.1"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002",
+ "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==7.4.2"
+ },
+ "pytest-html": {
+ "hashes": [
+ "sha256:88682b9e8e51392472546a70a2139b27d6bc1834a4afd3e41da33c9d9f91e4a4",
+ "sha256:907c3e68462df129d3ee96dee58bd63f70216b06421836b22fd3fd57ef314acb"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==4.0.2"
+ },
+ "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"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
+ "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.8.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.7"
+ }
+ },
+ "develop": {
+ "black": {
+ "hashes": [
+ "sha256:0e232f24a337fed7a82c1185ae46c56c4a6167fb0fe37411b43e876892c76699",
+ "sha256:30b78ac9b54cf87bcb9910ee3d499d2bc893afd52495066c49d9ee6b21eee06e",
+ "sha256:31946ec6f9c54ed7ba431c38bc81d758970dd734b96b8e8c2b17a367d7908171",
+ "sha256:31b9f87b277a68d0e99d2905edae08807c007973eaa609da5f0c62def6b7c0bd",
+ "sha256:47c4510f70ec2e8f9135ba490811c071419c115e46f143e4dce2ac45afdcf4c9",
+ "sha256:481167c60cd3e6b1cb8ef2aac0f76165843a374346aeeaa9d86765fe0dd0318b",
+ "sha256:6901631b937acbee93c75537e74f69463adaf34379a04eef32425b88aca88a23",
+ "sha256:76baba9281e5e5b230c9b7f83a96daf67a95e919c2dfc240d9e6295eab7b9204",
+ "sha256:7fb5fc36bb65160df21498d5a3dd330af8b6401be3f25af60c6ebfe23753f747",
+ "sha256:960c21555be135c4b37b7018d63d6248bdae8514e5c55b71e994ad37407f45b8",
+ "sha256:a3c2ddb35f71976a4cfeca558848c2f2f89abc86b06e8dd89b5a65c1e6c0f22a",
+ "sha256:c870bee76ad5f7a5ea7bd01dc646028d05568d33b0b09b7ecfc8ec0da3f3f39c",
+ "sha256:d3d9129ce05b0829730323bdcb00f928a448a124af5acf90aa94d9aba6969604",
+ "sha256:db451a3363b1e765c172c3fd86213a4ce63fb8524c938ebd82919bf2a6e28c6a",
+ "sha256:e223b731a0e025f8ef427dd79d8cd69c167da807f5710add30cdf131f13dd62e",
+ "sha256:f20ff03f3fdd2fd4460b4f631663813e57dc277e37fb216463f3b907aa5a9bdd",
+ "sha256:f74892b4b836e5162aa0452393112a574dac85e13902c57dfbaaf388e4eda37c",
+ "sha256:f8dc7d50d94063cdfd13c82368afd8588bac4ce360e4224ac399e769d6704e98"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==23.10.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"
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/SurveyExperimentIntegrationTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/SurveyExperimentIntegrationTest.kt
new file mode 100644
index 0000000000..1390887ba6
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/SurveyExperimentIntegrationTest.kt
@@ -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()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/__init__.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/conftest.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/conftest.py
new file mode 100644
index 0000000000..d6611dc8d0
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/conftest.py
@@ -0,0 +1,226 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import logging
+import os
+import subprocess
+import time
+from pathlib import Path
+
+import pytest
+import requests
+
+from experimentintegration.gradlewbuild import GradlewBuild
+from experimentintegration.models.models import TelemetryModel
+
+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()
+ branches = next(iter(data.get("branches")), None)
+ features = next(iter(branches.get("features")), None)
+ if features.get("messages"):
+ for item in features["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 = "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="check_ping_for_experiment")
+def fixture_check_ping_for_experiment(experiment_slug, variables):
+ def _check_ping_for_experiment(
+ branch=None, experiment=experiment_slug, reason=None
+ ):
+ model = TelemetryModel(branch=branch, experiment=experiment)
+
+ timeout = time.time() + 60 * 5
+ while time.time() < timeout:
+ data = requests.get(f"{variables['urls']['telemetry_server']}/pings").json()
+ events = []
+ for item in data:
+ event_items = item.get("events")
+ if event_items:
+ for event in event_items:
+ if (
+ "category" in event
+ and "nimbus_events" in event["category"]
+ and "extra" in event
+ and "branch" in event["extra"]
+ ):
+ events.append(event)
+ for event in events:
+ event_name = event.get("name")
+ if (reason == "enrollment" and event_name == "enrollment") or (
+ reason == "unenrollment"
+ and event_name in ["unenrollment", "disqualification"]
+ ):
+ telemetry_model = TelemetryModel(
+ branch=event["extra"]["branch"],
+ experiment=event["extra"]["experiment"],
+ )
+ if model == telemetry_model:
+ return True
+ time.sleep(5)
+ return False
+
+ return _check_ping_for_experiment
+
+
+@pytest.fixture(name="setup_experiment")
+def fixture_setup_experiment(experiment_slug, json_data, gradlewbuild_log, variables):
+ def _(branch):
+ requests.delete(f"{variables['urls']['telemetry_server']}/pings")
+ 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 _
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/generate_smoke_tests.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/generate_smoke_tests.py
new file mode 100644
index 0000000000..0998cbb49d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/generate_smoke_tests.py
@@ -0,0 +1,82 @@
+import subprocess
+from pathlib import Path
+
+import yaml
+
+
+def search_for_smoke_tests(tests_name):
+ """Searches for smoke tests within the requested test module."""
+ path = Path("../ui")
+ files = sorted([x for x in path.iterdir() if x.is_file()])
+ locations = []
+ file_name = None
+ test_names = []
+
+ for name in files:
+ if tests_name in name.name:
+ file_name = name
+ break
+
+ with open(file_name, "r") as file:
+ code = file.read().split(" ")
+ code = [item for item in code if item != ""]
+
+ for count, item in enumerate(code):
+ if "class" in item or "@SmokeTest" in item:
+ locations.append(count)
+
+ for location in locations:
+ if len(test_names) == 0:
+ class_name = code[location + 1]
+ test_names.append(class_name)
+ else:
+ test_names.append(f"{class_name}#{code[location+3].strip('()')}")
+ return test_names
+
+
+def create_test_file():
+ """Create the python file to hold the tests."""
+
+ path = Path("tests/")
+ filename = "test_smoke_scenarios.py"
+ final_path = path / filename
+
+ if final_path.exists():
+ print("File Exists, you need to delete it to create a new one.")
+ return
+ # file exists
+ subprocess.run([f"touch {final_path}"], encoding="utf8", shell=True)
+ assert final_path.exists()
+ with open(final_path, "w") as file:
+ file.write("import pytest\n\n")
+
+
+def generate_smoke_tests(tests_names=None):
+ """Generate pytest code for the requested tests."""
+ pytest_file = "tests/test_smoke_scenarios.py"
+ tests = []
+
+ for test in tests_names[1:]:
+ test_name = test.replace("#", "_").lower()
+ tests.append(
+ f"""
+@pytest.mark.smoke_test
+def test_smoke_{test_name}(setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment):
+ setup_experiment(load_branches)
+ gradlewbuild.test("{test}", smoke=True)
+ assert check_ping_for_experiment
+"""
+ )
+ with open(pytest_file, "a") as file:
+ for item in tests:
+ file.writelines(f"{item}")
+
+
+if __name__ == "__main__":
+ test_modules = None
+ create_test_file()
+ with open("variables.yaml", "r") as file:
+ test_modules = yaml.safe_load(file)
+ for item in test_modules.get("smoke_tests"):
+ tests = search_for_smoke_tests(item)
+ generate_smoke_tests(tests)
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/gradlewbuild.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/gradlewbuild.py
new file mode 100644
index 0000000000..43ac14a64c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/gradlewbuild.py
@@ -0,0 +1,50 @@
+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, smoke=None):
+ # self.adbrun.launch()
+
+ # Change path accordingly to go to root folder to run gradlew
+ os.chdir("../../../../../../../..")
+ test_type = "ui" if smoke else "experimentintegration"
+ cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.{test_type}.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
+ # if smoke:
+ # cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.ui.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
+ # else:
+ # 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))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/launchSimScript.sh b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/launchSimScript.sh
new file mode 100755
index 0000000000..648397f0ec
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/launchSimScript.sh
@@ -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
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/__init__.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/__init__.py
new file mode 100644
index 0000000000..6fbe8159b2
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/__init__.py
@@ -0,0 +1,3 @@
+# 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/.
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/models.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/models.py
new file mode 100644
index 0000000000..c0ea72253f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/models.py
@@ -0,0 +1,14 @@
+# 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/.
+
+"""Data class Models"""
+
+from pydantic import BaseModel
+
+
+class TelemetryModel(BaseModel):
+ """Experiment Telemetry model"""
+
+ branch: str
+ experiment: str
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/pytest.ini b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/pytest.ini
new file mode 100644
index 0000000000..40a205fb04
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+addopts = --verbose --html=results/index.html --self-contained-html --variables=variables.yaml
+log_cli = true
+log_cli_level = info
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/__init__.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_generic_scenarios.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_generic_scenarios.py
new file mode 100644
index 0000000000..b203970278
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_generic_scenarios.py
@@ -0,0 +1,25 @@
+import pytest
+
+
+@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
+def test_experiment_unenrolls_via_studies_toggle(
+ setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
+):
+ setup_experiment(load_branches)
+ gradlewbuild.test("GenericExperimentIntegrationTest#disableStudiesViaStudiesToggle")
+ assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
+ gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
+ assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0])
+
+
+@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
+def test_experiment_unenrolls_via_secret_menu(
+ setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
+):
+ setup_experiment(load_branches)
+ gradlewbuild.test(
+ "GenericExperimentIntegrationTest#testExperimentUnenrolledViaSecretMenu"
+ )
+ assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
+ gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
+ assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0])
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_survey_messages.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_survey_messages.py
new file mode 100644
index 0000000000..db08522a3e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_survey_messages.py
@@ -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/.
+
+import pytest
+
+
+@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
+def test_survey_navigates_correctly(
+ setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
+):
+ setup_experiment(load_branches)
+ gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyNavigatesCorrectly")
+ assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
+
+
+@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
+def test_survey_no_thanks_navigates_correctly(
+ setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
+):
+ setup_experiment(load_branches)
+ gradlewbuild.test(
+ "SurveyExperimentIntegrationTest#checkSurveyNoThanksNavigatesCorrectly"
+ )
+ assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
+
+
+@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
+def test_homescreen_survey_dismisses_correctly(
+ setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
+):
+ setup_experiment(load_branches)
+ gradlewbuild.test(
+ "SurveyExperimentIntegrationTest#checkHomescreenSurveyDismissesCorrectly"
+ )
+ assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
+
+
+@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
+def test_survey_landscape_looks_correct(
+ setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
+):
+ setup_experiment(load_branches)
+ gradlewbuild.test(
+ "SurveyExperimentIntegrationTest#checkSurveyLandscapeLooksCorrect"
+ )
+ assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/variables.yaml b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/variables.yaml
new file mode 100644
index 0000000000..2b8cf67b83
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/variables.yaml
@@ -0,0 +1,10 @@
+# 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/.
+
+urls:
+ stage_server: "https://stage.experimenter.nonprod.dataops.mozgcp.net"
+ prod_server: "https://experimenter.services.mozilla.com"
+ telemetry_server: "http://172.25.58.187:5000"
+smoke_tests:
+ - "AddressAutofillTest"
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/extensions/ExtensionProcessTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/extensions/ExtensionProcessTest.kt
new file mode 100644
index 0000000000..6fdf897b95
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/extensions/ExtensionProcessTest.kt
@@ -0,0 +1,34 @@
+/* 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.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.gecko.GeckoProvider
+import org.mozilla.fenix.helpers.TestHelper
+
+/**
+ * Instrumentation test for verifying that the extensions process is enabled unconditionally.
+ */
+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_is_enabled() {
+ val runtime = GeckoProvider.createRuntimeSettings(context, policy)
+ assertTrue(runtime.extensionsProcessEnabled!!)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/glean/BaselinePingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/glean/BaselinePingTest.kt
new file mode 100644
index 0000000000..0adf507fd9
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/glean/BaselinePingTest.kt
@@ -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 = 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().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."))
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/AppAndSystemHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/AppAndSystemHelper.kt
new file mode 100644
index 0000000000..ac40972db6
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/AppAndSystemHelper.kt
@@ -0,0 +1,589 @@
+/* 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.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.os.storage.StorageManager
+import android.os.storage.StorageVolume
+import android.provider.Settings
+import android.util.Log
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage
+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.UiObject
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import junit.framework.AssertionFailedError
+import kotlinx.coroutines.runBlocking
+import mozilla.appservices.places.BookmarkRoot
+import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
+import mozilla.components.browser.storage.sync.PlacesHistoryStorage
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.mozilla.fenix.Config
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.components.PermissionStorage
+import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
+import org.mozilla.fenix.helpers.Constants.PackageName.PIXEL_LAUNCHER
+import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
+import org.mozilla.fenix.ui.robots.BrowserRobot
+import org.mozilla.gecko.util.ThreadUtils
+import java.io.File
+import java.util.Locale
+
+object AppAndSystemHelper {
+
+ private val bookmarksStorage = PlacesBookmarksStorage(appContext.applicationContext)
+ suspend fun bookmarks() = bookmarksStorage.getTree(BookmarkRoot.Mobile.id)?.children
+ fun getPermissionAllowID(): String {
+ Log.i(TAG, "getPermissionAllowID: Trying to get the permission button resource ID based on API.")
+ return when
+ (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ true -> {
+ Log.i(TAG, "getPermissionAllowID: Getting the permission button resource ID for API ${Build.VERSION.SDK_INT}.")
+ "com.android.permissioncontroller"
+ }
+ false -> {
+ Log.i(TAG, "getPermissionAllowID: Getting the permission button resource ID for API ${Build.VERSION.SDK_INT}.")
+ "com.android.packageinstaller"
+ }
+ }
+ }
+
+ /**
+ * Checks if a specific download file is inside the device storage and deletes it.
+ * Different implementation needed for newer API levels,
+ * as Environment.getExternalStorageDirectory() is deprecated starting with API 29.
+ *
+ */
+ fun deleteDownloadedFileOnStorage(fileName: String) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ Log.i(TAG, "deleteDownloadedFileOnStorage: Trying to delete file from API ${Build.VERSION.SDK_INT}.")
+ 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()) {
+ Log.i(TAG, "deleteDownloadedFileOnStorage: The file exists. Trying to delete $fileName, try 1.")
+ file.delete()
+ Assert.assertFalse("$TAG deleteDownloadedFileOnStorage: The $fileName file was not deleted", file.exists())
+ Log.i(TAG, "deleteDownloadedFileOnStorage: Verified the $fileName file was deleted.")
+ }
+ } catch (e: AssertionError) {
+ Log.i(TAG, "deleteDownloadedFileOnStorage: AssertionError caught. Retrying to delete the file.")
+ file.delete()
+ Log.i(TAG, "deleteDownloadedFileOnStorage: Retrying to delete $fileName.")
+ Assert.assertFalse("$TAG deleteDownloadedFileOnStorage: The file was not deleted", file.exists())
+ Log.i(TAG, "deleteDownloadedFileOnStorage: Verified the $fileName file was deleted, try 2.")
+ }
+ } else {
+ runBlocking {
+ Log.i(TAG, "deleteDownloadedFileOnStorage: Trying to delete file from API ${Build.VERSION.SDK_INT}.")
+ val downloadedFile = File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
+ fileName,
+ )
+
+ if (downloadedFile.exists()) {
+ Log.i(TAG, "deleteDownloadedFileOnStorage: The file exists. Trying to delete the file.")
+ downloadedFile.delete()
+ Log.i(TAG, "deleteDownloadedFileOnStorage: $downloadedFile deleted.")
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if there are download files inside the device storage and deletes all of them.
+ * Different implementation needed for newer API levels, as
+ * Environment.getExternalStorageDirectory() is deprecated starting with API 29.
+ */
+ fun clearDownloadsFolder() {
+ Log.i(TAG, "clearDownloadsFolder: Detected API ${Build.VERSION.SDK_INT}")
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ val storageManager: StorageManager? =
+ appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
+ val storageVolumes = storageManager!!.storageVolumes
+ val storageVolume: StorageVolume = storageVolumes[0]
+ val downloadsFolder = File(storageVolume.directory!!.path + "/Download/")
+
+ // Check if the downloads folder exists
+ if (downloadsFolder.exists() && downloadsFolder.isDirectory) {
+ Log.i(TAG, "clearDownloadsFolder: Verified that \"DOWNLOADS\" folder exists.")
+ var files = downloadsFolder.listFiles()
+
+ // Check if the folder is not empty
+ if (files != null && files.isNotEmpty()) {
+ Log.i(
+ TAG,
+ "clearDownloadsFolder: Before cleanup: Downloads storage contains: ${files.size} file(s).",
+ )
+ // Delete all files in the folder
+ for (file in files!!) {
+ Log.i(
+ TAG,
+ "clearDownloadsFolder: Trying to delete $file from \"DOWNLOADS\" folder.",
+ )
+ file.delete()
+ Log.i(
+ TAG,
+ "clearDownloadsFolder: Deleted $file from \"DOWNLOADS\" folder.",
+ )
+ files = downloadsFolder.listFiles()
+ Log.i(
+ TAG,
+ "clearDownloadsFolder: After cleanup: Downloads storage contains: ${files?.size} file(s).",
+ )
+ }
+ } else {
+ Log.i(
+ TAG,
+ "clearDownloadsFolder: Downloads storage is empty.",
+ )
+ }
+ }
+ } else {
+ runBlocking {
+ Log.i(TAG, "clearDownloadsFolder: Verifying if any download files exist.")
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ .listFiles()?.forEach {
+ Log.i(TAG, "clearDownloadsFolder: Trying to delete from storage: $it.")
+ it.delete()
+ Log.i(TAG, "clearDownloadsFolder: Download file $it deleted.")
+ }
+ }
+ }
+ }
+
+ suspend fun deleteHistoryStorage() {
+ val historyStorage = PlacesHistoryStorage(appContext.applicationContext)
+ Log.i(
+ TAG,
+ "deleteHistoryStorage before cleanup: History storage contains: ${historyStorage.getVisited()}",
+ )
+ if (historyStorage.getVisited().isNotEmpty()) {
+ Log.i(TAG, "deleteHistoryStorage: Trying to delete all history storage.")
+ historyStorage.deleteEverything()
+ Log.i(
+ TAG,
+ "deleteHistoryStorage after cleanup: History storage contains: ${historyStorage.getVisited()}",
+ )
+ }
+ }
+
+ suspend fun deleteBookmarksStorage() {
+ val bookmarks = bookmarks()
+ Log.i(TAG, "deleteBookmarksStorage before cleanup: Bookmarks storage contains: $bookmarks")
+ if (bookmarks?.isNotEmpty() == true) {
+ bookmarks.forEach {
+ Log.i(
+ TAG,
+ "deleteBookmarksStorage: Trying to delete $it bookmark from storage.",
+ )
+ bookmarksStorage.deleteNode(it.guid)
+ Log.i(
+ TAG,
+ "deleteBookmarksStorage: Bookmark deleted. Bookmarks storage contains: ${bookmarks()}",
+ )
+ }
+ }
+ }
+
+ suspend fun deletePermissionsStorage() {
+ val permissionStorage = PermissionStorage(appContext.applicationContext)
+ Log.i(
+ TAG,
+ "deletePermissionsStorage: Trying to delete permissions. Permissions storage contains: ${permissionStorage.getSitePermissionsPaged()}",
+ )
+ permissionStorage.deleteAllSitePermissions()
+ Log.i(
+ TAG,
+ "deletePermissionsStorage: Permissions deleted. Permissions storage contains: ${permissionStorage.getSitePermissionsPaged()}",
+ )
+ }
+
+ fun setNetworkEnabled(enabled: Boolean) {
+ val networkDisconnectedIdlingResource = NetworkConnectionIdlingResource(false)
+ val networkConnectedIdlingResource = NetworkConnectionIdlingResource(true)
+
+ when (enabled) {
+ true -> {
+ Log.i(
+ TAG,
+ "setNetworkEnabled: Trying to enable the network connection.",
+ )
+ mDevice.executeShellCommand("svc data enable")
+ Log.i(
+ TAG,
+ "setNetworkEnabled: Data network connection enable command sent.",
+ )
+ mDevice.executeShellCommand("svc wifi enable")
+ Log.i(
+ TAG,
+ "setNetworkEnabled: Wifi network connection enable command sent.",
+ )
+
+ // Wait for network connection to be completely enabled
+ Log.i(TAG, "setNetworkEnabled: Waiting for connection to be enabled.")
+ IdlingRegistry.getInstance().register(networkConnectedIdlingResource)
+ Espresso.onIdle {
+ IdlingRegistry.getInstance().unregister(networkConnectedIdlingResource)
+ }
+ Log.i(TAG, "setNetworkEnabled: Network connection was enabled.")
+ }
+
+ false -> {
+ Log.i(
+ TAG,
+ "setNetworkEnabled: Trying to disable the network connection.",
+ )
+ mDevice.executeShellCommand("svc data disable")
+ Log.i(
+ TAG,
+ "setNetworkEnabled: Data network connection disable command sent.",
+ )
+ mDevice.executeShellCommand("svc wifi disable")
+ Log.i(
+ TAG,
+ "setNetworkEnabled: Wifi network connection disable command sent.",
+ )
+
+ // Wait for network connection to be completely disabled
+ Log.i(TAG, "setNetworkEnabled: Waiting for connection to be disabled.")
+ IdlingRegistry.getInstance().register(networkDisconnectedIdlingResource)
+ Espresso.onIdle {
+ IdlingRegistry.getInstance().unregister(networkDisconnectedIdlingResource)
+ }
+ Log.i(TAG, "setNetworkEnabled: Network connection was disabled.")
+ }
+ }
+ }
+
+ fun isPackageInstalled(packageName: String): Boolean {
+ Log.i(TAG, "isPackageInstalled: Trying to verify that $packageName is installed")
+ return try {
+ val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager
+ packageManager.getApplicationInfo(packageName, 0).enabled
+ Log.i(TAG, "isPackageInstalled: $packageName is installed.")
+ true
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.i(TAG, "isPackageInstalled: $packageName is not installed - ${e.message}")
+ false
+ }
+ }
+
+ fun assertExternalAppOpens(appPackageName: String) {
+ if (isPackageInstalled(appPackageName)) {
+ try {
+ Log.i(TAG, "assertExternalAppOpens: Trying to check the intent sent.")
+ intended(toPackage(appPackageName))
+ Log.i(TAG, "assertExternalAppOpens: Matched open intent to $appPackageName.")
+ } catch (e: AssertionFailedError) {
+ Log.i(TAG, "assertExternalAppOpens: Intent match failure. ${e.message}")
+ }
+ } else {
+ Log.i(TAG, "assertExternalAppOpens: Trying to verify the \"Could not open file\" message.")
+ mDevice.waitNotNull(
+ Until.findObject(By.text("Could not open file")),
+ waitingTime,
+ )
+ Log.i(TAG, "assertExternalAppOpens: Verified \"Could not open file\" message")
+ }
+ }
+
+ fun assertNativeAppOpens(appPackageName: String, url: String = "") {
+ if (isPackageInstalled(appPackageName)) {
+ Log.i(TAG, "assertNativeAppOpens: Waiting for the device to be idle $waitingTimeShort ms.")
+ mDevice.waitForIdle(waitingTimeShort)
+ Log.i(TAG, "assertNativeAppOpens: Waited for the device to be idle $waitingTimeShort ms.")
+ Log.i(TAG, "assertNativeAppOpens: Trying to match the app package name is matched.")
+ Assert.assertTrue(
+ "$TAG $appPackageName not found",
+ mDevice.findObject(UiSelector().packageName(appPackageName))
+ .waitForExists(waitingTime),
+ )
+ Log.i(TAG, "assertNativeAppOpens: App package name matched.")
+ } else {
+ Log.i(TAG, "assertNativeAppOpens: Trying to verify the page redirect URL.")
+ BrowserRobot().verifyUrl(url)
+ Log.i(TAG, "assertNativeAppOpens: Verified the page redirect URL.")
+ }
+ }
+
+ fun assertYoutubeAppOpens() {
+ Log.i(TAG, "assertYoutubeAppOpens: Trying to check the intent to YouTube.")
+ intended(toPackage(YOUTUBE_APP))
+ Log.i(TAG, "assertYoutubeAppOpens: Verified the intent matches YouTube.")
+ }
+
+ /**
+ * 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
+
+ Log.i(TAG, "isExternalAppBrowserActivityInCurrentTask: Waiting for the device to be idle for $waitingTimeShort ms")
+ mDevice.waitForIdle(waitingTimeShort)
+ Log.i(TAG, "isExternalAppBrowserActivityInCurrentTask: Waited for the device to be idle for $waitingTimeShort ms")
+
+ Log.i(
+ TAG,
+ "isExternalAppBrowserActivityInCurrentTask: Trying to verify that the latest activity of the application is used for custom tabs or PWAs",
+ )
+ 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 {
+ Log.i(TAG, "registerAndCleanupIdlingResources: Trying to register idling resource $it.")
+ IdlingRegistry.getInstance().register(it)
+ Log.i(TAG, "registerAndCleanupIdlingResources: Registered idling resource $it.")
+ }
+
+ try {
+ testBlock()
+ } finally {
+ idlingResources.forEach {
+ Log.i(TAG, "registerAndCleanupIdlingResources: Trying to unregister idling resource $it.")
+ IdlingRegistry.getInstance().unregister(it)
+ Log.i(TAG, "registerAndCleanupIdlingResources: Unregistered idling resource $it.")
+ }
+ }
+ }
+
+ // 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)) {
+ Log.i(TAG, "grantSystemPermission: Trying to click the \"While using the app\" button.")
+ whileUsingTheAppPermissionButton.click()
+ Log.i(TAG, "grantSystemPermission: Clicked the \"While using the app\" button.")
+ } else if (allowPermissionButton.waitForExists(waitingTimeShort)) {
+ Log.i(TAG, "grantSystemPermission: Trying to click the \"Allow\" button.")
+ allowPermissionButton.click()
+ Log.i(TAG, "grantSystemPermission: Clicked the \"Allow\" button.")
+ }
+ }
+ }
+
+ // Permission deny dialogs differ on various Android APIs
+ fun denyPermission() {
+ Log.i(TAG, "denyPermission: Waiting $waitingTime ms for the \"Deny\" button to exist.")
+ mDevice.findObject(UiSelector().textContains("Deny")).waitForExists(waitingTime)
+ Log.i(TAG, "denyPermission: Waited for $waitingTime ms for the \"Deny\" button to exist.")
+ Log.i(TAG, "denyPermission: Trying to click the \"Deny\" button.")
+ mDevice.findObject(UiSelector().textContains("Deny")).click()
+ Log.i(TAG, "denyPermission: Clicked the \"Deny\" button.")
+ }
+
+ fun isTestLab(): Boolean {
+ return Settings.System.getString(TestHelper.appContext.contentResolver, "firebase.test.lab").toBoolean()
+ }
+
+ /**
+ * Changes the default language of the entire device, not just the app.
+ * Runs the test in its testBlock.
+ * Cleans up and sets the default locale after it's done.
+ */
+ fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule, testBlock: () -> Unit) {
+ val defaultLocale = Locale.getDefault()
+
+ try {
+ Log.i(TAG, "runWithSystemLocaleChanged: Trying to set the locale.")
+ setSystemLocale(locale)
+ Log.i(TAG, "runWithSystemLocaleChanged: Running the test block.")
+ testBlock()
+ ThreadUtils.runOnUiThread { testRule.activity.recreate() }
+ Log.i(TAG, "runWithSystemLocaleChanged: Test block finished.")
+ } catch (e: Exception) {
+ Log.i(TAG, "runWithSystemLocaleChanged: The test block has thrown an exception.${e.message}")
+ e.printStackTrace()
+ } finally {
+ Log.i(TAG, "runWithSystemLocaleChanged: Trying to reset the locale to default.")
+ setSystemLocale(defaultLocale)
+ }
+ }
+
+ /**
+ * Changes the default language of the entire device, not just the app.
+ * We can only use this if we're running on a debug build, otherwise it will change the permission manifests in release builds.
+ */
+ fun setSystemLocale(locale: Locale) {
+ if (Config.channel.isDebug) {
+ /* Sets permission to change device language */
+ Log.i(
+ TAG,
+ "setSystemLocale: Requesting permission to change system locale to $locale.",
+ )
+ PermissionRequester().apply {
+ addPermissions(
+ Manifest.permission.CHANGE_CONFIGURATION,
+ )
+ requestPermissions()
+ }
+ Log.i(
+ TAG,
+ "setSystemLocale: Received permission to change system locale to $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)
+ }
+ Log.i(
+ TAG,
+ "setSystemLocale: Changed system locale to $locale.",
+ )
+ }
+
+ fun putAppToBackground() {
+ Log.i(TAG, "putAppToBackground: Trying to press the device Recent apps button.")
+ mDevice.pressRecentApps()
+ Log.i(TAG, "putAppToBackground: Pressed the device Recent apps button.")
+ Log.i(TAG, "putAppToBackground: Waiting for the app to be gone for $waitingTime ms.")
+ mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/container")).waitUntilGone(
+ waitingTime,
+ )
+ Log.i(TAG, "putAppToBackground: Waited for the app to be gone for $waitingTime ms.")
+ }
+
+ /**
+ * Brings the app to foregorund by clicking it in the recent apps tray.
+ * The package name is related to the home screen experience for the Pixel phones produced by Google.
+ * The recent apps tray on API 30 will always display only 2 apps, even if previously were opened more.
+ * The index of the most recent opened app will always have index 2, meaning that the previously opened app will have index 1.
+ */
+ fun bringAppToForeground() {
+ Log.i(TAG, "bringAppToForeground: Trying to select the app from the recent apps tray and wait for $waitingTime ms for a new window")
+ mDevice.findObject(UiSelector().index(2).packageName(PIXEL_LAUNCHER))
+ .clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "bringAppToForeground: Selected the app from the recent apps tray.")
+ }
+
+ fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) {
+ Log.i(TAG, "verifyKeyboardVisibility: Waiting for the device to be idle.")
+ mDevice.waitForIdle()
+ Log.i(TAG, "verifyKeyboardVisibility: Waited for the device to be idle.")
+
+ Log.i(TAG, "verifyKeyboardVisibility: Trying to verify the keyboard is visible.")
+ assertEquals(
+ "Keyboard not shown",
+ isExpectedToBeVisible,
+ mDevice
+ .executeShellCommand("dumpsys input_method | grep mInputShown")
+ .contains("mInputShown=true"),
+ )
+ Log.i(TAG, "verifyKeyboardVisibility: Verified the keyboard is visible.")
+ }
+
+ fun openAppFromExternalLink(url: String) {
+ val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+ val intent = Intent().apply {
+ action = Intent.ACTION_VIEW
+ data = Uri.parse(url)
+ `package` = TestHelper.packageName
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ try {
+ Log.i(TAG, "openAppFromExternalLink: Trying to start the activity from an external intent.")
+ context.startActivity(intent)
+ Log.i(TAG, "openAppFromExternalLink: Activity started from an external intent.")
+ } catch (ex: ActivityNotFoundException) {
+ Log.i(TAG, "openAppFromExternalLink: Exception caught. Trying to start the activity from a null intent.")
+ intent.setPackage(null)
+ context.startActivity(intent)
+ Log.i(TAG, "openAppFromExternalLink: Started the activity from a null intent.")
+ }
+ }
+
+ /**
+ * 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) {
+ Log.i(TAG, "runWithCondition: Trying to run the test based on condition. The condition is: $condition.")
+ if (condition) {
+ testBlock()
+ }
+ }
+
+ /**
+ * Wrapper to launch the app using the launcher intent.
+ */
+ fun runWithLauncherIntent(
+ activityTestRule: AndroidComposeTestRule,
+ testBlock: () -> Unit,
+ ) {
+ val launcherIntent = Intent(Intent.ACTION_MAIN).apply {
+ addCategory(Intent.CATEGORY_LAUNCHER)
+ }
+
+ Log.i(TAG, "runWithLauncherIntent: Trying to launch the activity from an intent: $launcherIntent.")
+ activityTestRule.activityRule.withIntent(launcherIntent).launchActivity(launcherIntent)
+ Log.i(TAG, "runWithLauncherIntent: Launched the activity from an intent: $launcherIntent.")
+ try {
+ Log.i(TAG, "runWithLauncherIntent: Trying run the test block.")
+ testBlock()
+ Log.i(TAG, "runWithLauncherIntent: Finished running the test block.")
+ } catch (e: Exception) {
+ Log.i(TAG, "runWithLauncherIntent: Exception caught while running the test block: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Assert.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Assert.kt
new file mode 100644
index 0000000000..b0520567d4
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Assert.kt
@@ -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.graphics.Bitmap
+import android.graphics.Color
+import android.util.Log
+import org.junit.Assert.assertEquals
+import org.mozilla.fenix.helpers.Constants.TAG
+
+/**
+ * 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) {
+ Log.i(TAG, "assertEqualsWithDelta: Trying to verify that the Bitmap of $expectedB is equal with the Bitmap of $actualB within delta: $delta")
+ 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)
+ }
+ }
+ Log.i(TAG, "assertEqualsWithDelta: Verified that the Bitmap of $expectedB is equal with the Bitmap of $actualB within delta: $delta")
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt
new file mode 100644
index 0000000000..037331d5b6
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt
@@ -0,0 +1,53 @@
+/* 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.DataGenerationHelper.getSponsoredShortcutTitle
+
+object Constants {
+
+ // Tag used for logging
+ const val TAG = "MozUITestLog"
+
+ // 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 PRINT_SPOOLER = "com.android.printspooler"
+ const val PIXEL_LAUNCHER = "com.google.android.apps.nexuslauncher"
+ }
+
+ 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",
+ )
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/DataGenerationHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/DataGenerationHelper.kt
new file mode 100644
index 0000000000..44e46b1355
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/DataGenerationHelper.kt
@@ -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.helpers
+
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.net.Uri
+import android.util.Log
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiSelector
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.state.availableSearchEngines
+import org.junit.Assert
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.utils.IntentUtils
+import java.time.LocalDate
+import java.time.LocalTime
+
+object DataGenerationHelper {
+ val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ fun createCustomTabIntent(
+ pageUrl: String,
+ customMenuItemLabel: String = "",
+ customActionButtonDescription: String = "",
+ ): Intent {
+ Log.i(TAG, "createCustomTabIntent: Trying to create custom tab intent with url: $pageUrl")
+ 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)
+ Log.i(TAG, "createCustomTabIntent: Created custom tab intent with url: $pageUrl")
+ return customTabsIntent.intent
+ }
+
+ private fun createTestBitmap(): Bitmap {
+ Log.i(TAG, "createTestBitmap: Trying to create a test bitmap")
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+ canvas.drawColor(Color.GREEN)
+ Log.i(TAG, "createTestBitmap: Created a test bitmap")
+ return bitmap
+ }
+
+ fun getStringResource(id: Int, argument: String = TestHelper.appName) = TestHelper.appContext.resources.getString(id, argument)
+
+ private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9')
+ fun generateRandomString(stringLength: Int): String {
+ Log.i(TAG, "generateRandomString: Trying to generate a random string with $stringLength characters")
+ val randomString =
+ (1..stringLength)
+ .map { kotlin.random.Random.nextInt(0, charPool.size) }
+ .map(charPool::get)
+ .joinToString("")
+ Log.i(TAG, "generateRandomString: Generated random string: $randomString")
+
+ return randomString
+ }
+
+ /**
+ * Creates clipboard data.
+ */
+ fun setTextToClipBoard(context: Context, message: String) {
+ Log.i(TAG, "setTextToClipBoard: Trying to set clipboard text to: $message")
+ val clipBoard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clipData = ClipData.newPlainText("label", message)
+
+ clipBoard.setPrimaryClip(clipData)
+ Log.i(TAG, "setTextToClipBoard: Clipboard text was set to: $message")
+ }
+
+ /**
+ * Constructs a date and time placeholder string for sponsored Fx suggest links.
+ * The format of the datetime is YYYYMMDDHH, where YYYY is the four-digit year,
+ * MM is the two-digit month, DD is the two-digit day, and HH is the two-digit hour.
+ * Single-digit months, days, and hours are padded with a leading zero to ensure
+ * the correct format. For example, a date and time of January 10, 2024, at 3 PM
+ * would be represented as "2024011015".
+ *
+ * @return A string representing the current date and time in the specified format.
+ */
+ fun getSponsoredFxSuggestPlaceHolder(): String {
+ Log.i(TAG, "getSponsoredFxSuggestPlaceHolder: Trying to get the sponsored search suggestion placeholder")
+ val currentDate = LocalDate.now()
+ val currentTime = LocalTime.now()
+
+ val currentDay = currentDate.dayOfMonth.toString().padStart(2, '0')
+ val currentMonth = currentDate.monthValue.toString().padStart(2, '0')
+ val currentYear = currentDate.year.toString()
+ val currentHour = currentTime.hour.toString().padStart(2, '0')
+
+ Log.i(TAG, "getSponsoredFxSuggestPlaceHolder: Got: ${currentYear + currentMonth + currentDay + currentHour} as the sponsored search suggestion placeholder")
+
+ return currentYear + currentMonth + currentDay + currentHour
+ }
+
+ /**
+ * Returns sponsored shortcut title based on the index.
+ */
+ fun getSponsoredShortcutTitle(position: Int): String {
+ Log.i(TAG, "getSponsoredShortcutTitle: Trying to get the title of the sponsored shortcut at position: ${position - 1}")
+ val sponsoredShortcut = mDevice.findObject(
+ UiSelector()
+ .resourceId("${TestHelper.packageName}:id/top_site_item")
+ .index(position - 1),
+ ).getChild(
+ UiSelector()
+ .resourceId("${TestHelper.packageName}:id/top_site_title"),
+ ).text
+ Log.i(TAG, "getSponsoredShortcutTitle: The sponsored shortcut at position: ${position - 1} has title: $sponsoredShortcut")
+ return sponsoredShortcut
+ }
+
+ /**
+ * 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 {
+ Log.i(TAG, "getRegionSearchEnginesList: Trying to get the search engines based on the region of the user")
+ val searchEnginesList = appContext.components.core.store.state.search.regionSearchEngines
+ Assert.assertTrue("$TAG: Search engines list returned nothing", searchEnginesList.isNotEmpty())
+ Log.i(TAG, "getRegionSearchEnginesList: Got $searchEnginesList based on the region of the user")
+ 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 {
+ Log.i(TAG, "getAvailableSearchEngines: Trying to get the alternative search engines based on the region of the user")
+ val searchEnginesList = TestHelper.appContext.components.core.store.state.search.availableSearchEngines
+ Assert.assertTrue("$TAG: Search engines list returned nothing", searchEnginesList.isNotEmpty())
+ Log.i(TAG, "getAvailableSearchEngines: Got $searchEnginesList based on the region of the user")
+ return searchEnginesList
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Experimentation.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Experimentation.kt
new file mode 100644
index 0000000000..4c320a8687
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Experimentation.kt
@@ -0,0 +1,16 @@
+/* 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.NimbusMessagingHelperInterface
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.TestHelper.appContext
+
+object Experimentation {
+ fun withHelper(block: NimbusMessagingHelperInterface.() -> Unit) {
+ val helper = appContext.components.nimbus.createJexlHelper()
+ block(helper)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt
new file mode 100644
index 0000000000..9b6fda9e1e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt
@@ -0,0 +1,101 @@
+/* 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 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,
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt
new file mode 100644
index 0000000000..b1e8debec8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt
@@ -0,0 +1,190 @@
+/* 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.util.Log
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.getPreferenceKey
+import org.mozilla.fenix.helpers.Constants.TAG
+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,
+ 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 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() {
+ Log.i(TAG, "applyFlagUpdates: Trying to apply the updated feature flags: $updatedFeatureFlags")
+ applyFeatureFlags(updatedFeatureFlags)
+ Log.i(TAG, "applyFlagUpdates: Applied the updated feature flags: $updatedFeatureFlags")
+ }
+
+ override fun resetAllFeatureFlags() {
+ Log.i(TAG, "resetAllFeatureFlags: Trying to reset the feature flags to: $initialFeatureFlags")
+ applyFeatureFlags(initialFeatureFlags)
+ Log.i(TAG, "resetAllFeatureFlags: Performed feature flags reset to: $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.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 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 -> {
+ Log.i(TAG, "setETPPolicy: Trying to set ETP policy to: \"Strict\"")
+ settings.setStrictETP()
+ Log.i(TAG, "setETPPolicy: ETP policy was set to: \"Strict\"")
+ }
+ // The following two cases update ETP in the same way "setStrictETP" does.
+ STANDARD -> {
+ Log.i(TAG, "setETPPolicy: Trying to set ETP policy to: \"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()
+ Log.i(TAG, "setETPPolicy: ETP policy was set to: \"Standard\"")
+ }
+ CUSTOM -> {
+ Log.i(TAG, "setETPPolicy: Trying to set ETP policy to: \"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()
+ Log.i(TAG, "setETPPolicy: ETP policy was set to: \"Custom\"")
+ }
+ }
+}
+
+private fun getHomeOnboardingVersion(): Int {
+ Log.i(TAG, "getHomeOnboardingVersion: Trying to get the onboarding version")
+ return FenixOnboarding(appContext)
+ .preferences
+ .getInt(FenixOnboarding.LAST_VERSION_ONBOARDING_KEY, 0)
+}
+
+private fun setHomeOnboardingVersion(version: Int) {
+ Log.i(TAG, "setHomeOnboardingVersion: Trying to set the onboarding version to: $version")
+ FenixOnboarding(appContext)
+ .preferences.edit()
+ .putInt(FenixOnboarding.LAST_VERSION_ONBOARDING_KEY, version)
+ .commit()
+ Log.i(TAG, "setHomeOnboardingVersion: Onboarding version was set to: $version")
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt
new file mode 100644
index 0000000000..2d96ecabdd
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt
@@ -0,0 +1,326 @@
+/* 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.util.Log
+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.Constants.TAG
+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
+
+/**
+ * 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::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,
+ 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.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
+ this.etpPolicy = etpPolicy
+ this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
+ this.composeTopSitesEnabled = composeTopSitesEnabled
+ }
+
+ /**
+ * Update settings after the activity was created.
+ */
+ fun applySettingsExceptions(settings: (FeatureSettingsHelper) -> Unit) {
+ Log.i(TAG, "applySettingsExceptions: Trying to update the settings after the activity was created")
+ FeatureSettingsHelperDelegate().also {
+ settings(this)
+ applyFlagUpdates()
+ }
+ Log.i(TAG, "applySettingsExceptions: Updated the settings after the activity was created")
+ }
+
+ private val longTapUserPreference = getLongPressTimeout()
+
+ override fun beforeActivityLaunched() {
+ super.beforeActivityLaunched()
+ setLongTapTimeout(3000)
+ Log.i(TAG, "beforeActivityLaunched: Trying to apply the feature flags updates")
+ applyFlagUpdates()
+ Log.i(TAG, "beforeActivityLaunched: Successfully applied the feature flag updates")
+ if (skipOnboarding) { skipOnboardingBeforeLaunch() }
+ }
+
+ override fun afterActivityFinished() {
+ super.afterActivityFinished()
+ setLongTapTimeout(longTapUserPreference)
+ Log.i(TAG, "afterActivityFinished: Trying to reset all feature flags")
+ resetAllFeatureFlags()
+ Log.i(TAG, "afterActivityFinished: Successfully performed the reset of all feature flags")
+ 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,
+ 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::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,
+ 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.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) {
+ Log.i(TAG, "applySettingsExceptions: Trying to update the settings after the activity was created")
+ FeatureSettingsHelperDelegate().apply {
+ settings(this)
+ applyFlagUpdates()
+ }
+ Log.i(TAG, "applySettingsExceptions: Updated the settings after the activity was created")
+ }
+
+ 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)
+ Log.i(TAG, "beforeActivityLaunched: Trying to apply the feature flag updates")
+ applyFlagUpdates()
+ Log.i(TAG, "beforeActivityLaunched: Successfully applied the feature flag updates")
+ if (skipOnboarding) { skipOnboardingBeforeLaunch() }
+ }
+
+ override fun afterActivityFinished() {
+ super.afterActivityFinished()
+ setLongTapTimeout(longTapUserPreference)
+ closeNotificationShade()
+ Log.i(TAG, "afterActivityFinished: Trying to reset all feature flags")
+ resetAllFeatureFlags()
+ Log.i(TAG, "afterActivityFinished: Successfully performed the reset of all feature flags")
+ }
+
+ /**
+ * 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
+ 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,
+ 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 {
+ Log.i(TAG, "setLongTapTimeout: Trying to set the \"Touch and hold delay\" to: $delay ms")
+ mDevice.executeShellCommand("settings put secure long_press_timeout $delay")
+ Log.i(TAG, "setLongTapTimeout: Executed command \"settings put secure long_press_timeout $delay\"")
+ break
+ } catch (e: RuntimeException) {
+ Log.i(TAG, "setLongTapTimeout: RuntimeException caught, executing fallback methods")
+ e.printStackTrace()
+ }
+ }
+}
+
+private fun skipOnboardingBeforeLaunch() {
+ // The production code isn't aware that we're using
+ // this API so it can be fragile.
+ Log.i(TAG, "skipOnboardingBeforeLaunch: Trying to skip the onboarding before launching the app")
+ FenixOnboarding(appContext).finish()
+ Log.i(TAG, "skipOnboardingBeforeLaunch: Successfully skipped the onboarding before launching the app")
+}
+
+private fun closeNotificationShade() {
+ if (mDevice.findObject(
+ UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller"),
+ ).exists()
+ ) {
+ Log.i(TAG, "closeNotificationShade: Trying to press device home button")
+ mDevice.pressHome()
+ Log.i(TAG, "closeNotificationShade: Pressed the device home button")
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt
new file mode 100644
index 0000000000..fc3d434277
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt
@@ -0,0 +1,21 @@
+/* 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.util.Log
+import androidx.test.espresso.IdlingRegistry
+import org.mozilla.fenix.helpers.Constants.TAG
+
+object IdlingResourceHelper {
+ fun unregisterAllIdlingResources() {
+ for (resource in IdlingRegistry.getInstance().resources) {
+ Log.i(TAG, "unregisterAllIdlingResources: Trying to unregister ${resource.name} resource")
+ IdlingRegistry.getInstance().unregister(resource)
+ Log.i(TAG, "unregisterAllIdlingResources: Unregistered ${resource.name} resource")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MatcherHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MatcherHelper.kt
new file mode 100644
index 0000000000..ba9aa4ab8a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MatcherHelper.kt
@@ -0,0 +1,156 @@
+/* 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.util.Log
+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.Constants.TAG
+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): UiObject {
+ Log.i(TAG, "Looking for item with resource id: $resourceId")
+ return mDevice.findObject(UiSelector().resourceId(resourceId))
+ }
+
+ fun itemContainingText(itemText: String): UiObject {
+ Log.i(TAG, "Looking for item with text: $itemText")
+ return mDevice.findObject(UiSelector().textContains(itemText))
+ }
+
+ fun itemWithText(itemText: String): UiObject {
+ Log.i(TAG, "Looking for item with text: $itemText")
+ return mDevice.findObject(UiSelector().text(itemText))
+ }
+
+ fun itemWithDescription(description: String): UiObject {
+ Log.i(TAG, "Looking for item with description: $description")
+ return mDevice.findObject(UiSelector().descriptionContains(description))
+ }
+
+ fun itemWithIndex(index: Int): UiObject {
+ Log.i(TAG, "Looking for item with index: $index")
+ return mDevice.findObject(UiSelector().index(index))
+ }
+
+ fun itemWithClassName(className: String): UiObject {
+ Log.i(TAG, "Looking for item with class name: $className")
+ return mDevice.findObject(UiSelector().className(className))
+ }
+
+ fun itemWithResIdAndIndex(resourceId: String, index: Int): UiObject {
+ Log.i(TAG, "Looking for item with resource id: $resourceId and index: $index")
+ return mDevice.findObject(UiSelector().resourceId(resourceId).index(index))
+ }
+
+ fun itemWithClassNameAndIndex(className: String, index: Int): UiObject {
+ Log.i(TAG, "Looking for item with class name: $className and index: $index")
+ return mDevice.findObject(UiSelector().className(className).index(index))
+ }
+
+ fun checkedItemWithResId(resourceId: String, isChecked: Boolean): UiObject {
+ Log.i(TAG, "Looking for checked item with resource id: $resourceId")
+ return mDevice.findObject(UiSelector().resourceId(resourceId).checked(isChecked))
+ }
+
+ fun checkedItemWithResIdAndText(resourceId: String, text: String, isChecked: Boolean): UiObject {
+ Log.i(TAG, "Looking for checked item with resource id: $resourceId and text: $text")
+ return mDevice.findObject(
+ UiSelector()
+ .resourceId(resourceId)
+ .textContains(text)
+ .checked(isChecked),
+ )
+ }
+
+ fun itemWithResIdAndDescription(resourceId: String, description: String): UiObject {
+ Log.i(TAG, "Looking for item with resource id: $resourceId and description: $description")
+ return mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description))
+ }
+
+ fun itemWithResIdAndText(resourceId: String, text: String): UiObject {
+ Log.i(TAG, "Looking for item with resource id: $resourceId and text: $text")
+ return mDevice.findObject(UiSelector().resourceId(resourceId).text(text))
+ }
+
+ fun itemWithResIdContainingText(resourceId: String, text: String): UiObject {
+ Log.i(TAG, "Looking for item with resource id: $resourceId and containing text: $text")
+ return mDevice.findObject(UiSelector().resourceId(resourceId).textContains(text))
+ }
+
+ fun assertUIObjectExists(
+ vararg appItems: UiObject,
+ exists: Boolean = true,
+ waitingTime: Long = TestAssetHelper.waitingTime,
+ ) {
+ for (appItem in appItems) {
+ if (exists) {
+ Log.i(TAG, "assertUIObjectExists: Trying to verify that ${appItem.selector} exists")
+ assertTrue("${appItem.selector} does not exist", appItem.waitForExists(waitingTime))
+ Log.i(TAG, "assertUIObjectExists: Verified that ${appItem.selector} exists")
+ } else {
+ Log.i(TAG, "assertUIObjectExists: Trying to verify that ${appItem.selector} does not exist")
+ assertFalse("${appItem.selector} exists", appItem.waitForExists(waitingTimeShort))
+ Log.i(TAG, "assertUIObjectExists: Verified that ${appItem.selector} does not exist")
+ }
+ }
+ }
+
+ fun assertUIObjectIsGone(vararg appItems: UiObject) {
+ for (appItem in appItems) {
+ Log.i(TAG, "assertUIObjectIsGone: Trying to verify that ${appItem.selector} is gone")
+ assertTrue("${appItem.selector} is not gone", appItem.waitUntilGone(waitingTime))
+ Log.i(TAG, "assertUIObjectIsGone: Verified that ${appItem.selector} is gone")
+ }
+ }
+
+ fun assertItemTextEquals(vararg appItems: UiObject, expectedText: String, isEqual: Boolean = true) {
+ for (appItem in appItems) {
+ if (isEqual) {
+ Log.i(TAG, "assertItemTextEquals: Trying to verify that ${appItem.selector} text equals to $expectedText")
+ assertTrue(
+ "${appItem.selector} text does not equal to $expectedText",
+ appItem.text.equals(expectedText),
+ )
+ Log.i(TAG, "assertItemTextEquals: Verified ${appItem.selector} text equals to $expectedText")
+ } else {
+ Log.i(TAG, "assertItemTextEquals: Trying to verify that ${appItem.selector} text does not equal to $expectedText")
+ assertFalse(
+ "${appItem.selector} text equals to $expectedText",
+ appItem.text.equals(expectedText),
+ )
+ Log.i(TAG, "assertItemTextEquals: Verified that ${appItem.selector} text does not equal to $expectedText")
+ }
+ }
+ }
+
+ fun assertItemTextContains(vararg appItems: UiObject, itemText: String) {
+ for (appItem in appItems) {
+ Log.i(TAG, "assertItemTextContains: Trying to verify that ${appItem.selector} text contains $itemText")
+ assertTrue(
+ "${appItem.selector} text does not contain $itemText",
+ appItem.text.contains(itemText),
+ )
+ Log.i(TAG, "assertItemTextContains: Verified ${appItem.selector} text contains $itemText")
+ }
+ }
+
+ fun assertItemIsEnabledAndVisible(vararg appItems: UiObject) {
+ for (appItem in appItems) {
+ Log.i(TAG, "assertItemIsEnabledAndVisible: Trying to verify that ${appItem.selector} is visible and enabled")
+ assertTrue(appItem.waitForExists(waitingTime) && appItem.isEnabled)
+ Log.i(TAG, "assertItemIsEnabledAndVisible: Verified ${appItem.selector} is visible and enabled")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Matchers.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Matchers.kt
new file mode 100644
index 0000000000..71e21f4881
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/Matchers.kt
@@ -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 = maybeInvertMatcher(espressoIsEnabled(), isEnabled)
+
+/**
+ * The [espressoIsChecked] function that can also handle unchecked state through the boolean argument.
+ */
+fun isChecked(isChecked: Boolean): Matcher = maybeInvertMatcher(espressoIsChecked(), isChecked)
+
+/**
+ * The [espressoIsSelected] function that can also handle not selected state through the boolean argument.
+ */
+fun isSelected(isSelected: Boolean): Matcher = maybeInvertMatcher(espressoIsSelected(), isSelected)
+
+private fun maybeInvertMatcher(matcher: Matcher, useUnmodifiedMatcher: Boolean): Matcher = when {
+ useUnmodifiedMatcher -> matcher
+ else -> not(matcher)
+}
+
+fun withBitmapDrawable(bitmap: Bitmap, name: String): Matcher? = BitmapDrawableMatcher(bitmap, name)
+
+fun nthChildOf(
+ parentMatcher: Matcher,
+ childPosition: Int,
+): Matcher {
+ return object : TypeSafeMatcher() {
+ 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): Matcher? {
+ return object : BoundedMatcher(
+ 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)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockBrowserDataHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockBrowserDataHelper.kt
new file mode 100644
index 0000000000..0b5563fe29
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockBrowserDataHelper.kt
@@ -0,0 +1,133 @@
+/* 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.util.Log
+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.Constants.TAG
+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?) {
+ Log.i(TAG, "createBookmarkItem: Trying to add bookmark item at position: $position, with url: $url, and with title: $title")
+ runBlocking {
+ PlacesBookmarksStorage(context)
+ .addItem(
+ BookmarkRoot.Mobile.id,
+ url,
+ title,
+ position,
+ )
+ }
+ Log.i(TAG, "createBookmarkItem: Added bookmark item at position: $position, with url: $url, and with title: $title")
+ }
+
+ /**
+ * 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) {
+ Log.i(TAG, "createHistoryItem: Trying to add history item with url: $url")
+ runBlocking {
+ PlacesHistoryStorage(appContext)
+ .recordVisit(
+ url,
+ PageVisit(VisitType.LINK),
+ )
+ }
+ Log.i(TAG, "createHistoryItem: Added history item with url: $url")
+ }
+
+ /**
+ * 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) {
+ Log.i(TAG, "createTabItem: Trying to create a new tab with url: $url")
+ runBlocking {
+ appContext.components.useCases.tabsUseCases.addTab(url)
+ }
+ Log.i(TAG, "createTabItem: Created a new tab with url: $url")
+ }
+
+ /**
+ * Triggers a search for the provided search term in a new tab.
+ *
+ */
+ fun createSearchHistory(searchTerm: String) {
+ Log.i(TAG, "createSearchHistory: Trying to perform a new search with search term: $searchTerm")
+ appContext.components.useCases.searchUseCases.newTabSearch.invoke(searchTerm)
+ Log.i(TAG, "createSearchHistory: Performed a new search with search term: $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}"
+ Log.i(TAG, "createCustomSearchEngine: Trying to create a custom search engine named: $searchEngineName and search string: $searchString")
+ 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)
+ Log.i(TAG, "addCustomSearchEngine: Trying to add a custom search engine named: $searchEngineName")
+ appContext.components.useCases.searchUseCases.addSearchEngine(searchEngine)
+ Log.i(TAG, "addCustomSearchEngine: Added a custom search engine named: $searchEngineName")
+ }
+
+ /**
+ * 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)
+ Log.i(TAG, "setCustomSearchEngine: Trying to set a custom search engine named: $searchEngineName")
+ with(appContext.components.useCases.searchUseCases) {
+ addSearchEngine(searchEngine)
+ selectSearchEngine(searchEngine)
+ }
+ Log.i(TAG, "setCustomSearchEngine: A custom search engine named: $searchEngineName was set")
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockLocationUpdatesRule.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockLocationUpdatesRule.kt
new file mode 100644
index 0000000000..bf4af2a479
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockLocationUpdatesRule.kt
@@ -0,0 +1,119 @@
+/* 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.Constants.TAG
+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() {
+ Log.i(TAG, "MockLocationUpdatesRule: Trying to enable the mock location setting on the device")
+ /* 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.
+ */
+ Log.i(TAG, "MockLocationUpdatesRule: Trying to execute cmd \"appops set ${appContext.packageName} android:mock_location allow\"")
+ mDevice.executeShellCommand(
+ "appops set " +
+ appContext.packageName +
+ " android:mock_location allow",
+ )
+ Log.i(TAG, "MockLocationUpdatesRule: Executed cmd \"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.i(TAG, "MockLocationUpdatesRule: Exception $ex caught, addTestProvider failed")
+ }
+ locationManager.setTestProviderEnabled(mockProviderName, true)
+ Log.i(TAG, "MockLocationUpdatesRule: Enabled the mock location setting on the device")
+ }
+
+ // Cleaning up the location provider after the test.
+ override fun after() {
+ Log.i(TAG, "MockLocationUpdatesRule: Trying to clean up the location provider")
+ locationManager.setTestProviderEnabled(mockProviderName, false)
+ locationManager.removeTestProvider(mockProviderName)
+ Log.i(TAG, "MockLocationUpdatesRule: Cleaned up the location provider")
+ }
+
+ /**
+ * 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) {
+ Log.i(TAG, "setMockLocation: Trying to set the mock location")
+ 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)
+ Log.i(TAG, "setMockLocation: The mock location was successfully set")
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockWebServer.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockWebServer.kt
new file mode 100644
index 0000000000..9e919a72fe
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/MockWebServer.kt
@@ -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 {
+ val mockServer = MockWebServer()
+ var uniquePath = 0
+ val uris = mutableListOf()
+ messages.forEach { message ->
+ val response = MockResponse().setBody("$message")
+ 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"
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RecyclerViewIdlingResource.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RecyclerViewIdlingResource.kt
new file mode 100644
index 0000000000..4d39f9b82a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RecyclerViewIdlingResource.kt
@@ -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.helpers
+
+import android.util.Log
+import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.IdlingResource.ResourceCallback
+import org.mozilla.fenix.helpers.Constants.TAG
+
+class RecyclerViewIdlingResource(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) {
+ Log.i(TAG, "RecyclerViewIdlingResource: Trying to verify that the resource transitioned from busy to idle")
+ callback!!.onTransitionToIdle()
+ Log.i(TAG, "RecyclerViewIdlingResource: The resource transitioned to idle")
+ }
+ return true
+ }
+ return false
+ }
+
+ override fun registerIdleTransitionCallback(callback: ResourceCallback) {
+ this.callback = callback
+ Log.i(TAG, "RecyclerViewIdlingResource: Notified asynchronously that the resource is transitioning from busy to idle")
+ }
+
+ override fun getName(): String {
+ Log.i(TAG, "RecyclerViewIdlingResource: Trying to return the the name of the resource: ${RecyclerViewIdlingResource::class.java.name + ":" + recycler.id}")
+ return RecyclerViewIdlingResource::class.java.name + ":" + recycler.id
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt
new file mode 100644
index 0000000000..8399da56bc
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt
@@ -0,0 +1,100 @@
+/* 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.util.Log
+import androidx.test.espresso.IdlingResourceTimeoutException
+import androidx.test.espresso.NoMatchingViewException
+import androidx.test.uiautomator.UiObjectNotFoundException
+import junit.framework.AssertionFailedError
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+
+/**
+ * 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 {
+ @Suppress("TooGenericExceptionCaught", "ComplexMethod")
+ override fun apply(base: Statement, description: Description): Statement {
+ return statement {
+ for (i in 1..retryCount) {
+ try {
+ Log.i(TAG, "RetryTestRule: Started try #$i.")
+ base.evaluate()
+ break
+ } catch (t: AssertionError) {
+ Log.i(TAG, "RetryTestRule: AssertionError caught, retrying the UI test")
+ unregisterAllIdlingResources()
+ exitMenu()
+ if (i == retryCount) {
+ Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
+ throw t
+ }
+ } catch (t: AssertionFailedError) {
+ Log.i(TAG, "RetryTestRule: AssertionFailedError caught, retrying the UI test")
+ unregisterAllIdlingResources()
+ exitMenu()
+ if (i == retryCount) {
+ Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
+ throw t
+ }
+ } catch (t: UiObjectNotFoundException) {
+ Log.i(TAG, "RetryTestRule: UiObjectNotFoundException caught, retrying the UI test")
+ unregisterAllIdlingResources()
+ exitMenu()
+ if (i == retryCount) {
+ Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
+ throw t
+ }
+ } catch (t: NoMatchingViewException) {
+ Log.i(TAG, "RetryTestRule: NoMatchingViewException caught, retrying the UI test")
+ unregisterAllIdlingResources()
+ exitMenu()
+ if (i == retryCount) {
+ Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
+ throw t
+ }
+ } catch (t: IdlingResourceTimeoutException) {
+ Log.i(TAG, "RetryTestRule: IdlingResourceTimeoutException caught, retrying the UI test")
+ unregisterAllIdlingResources()
+ exitMenu()
+ if (i == retryCount) {
+ Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
+ throw t
+ }
+ } catch (t: RuntimeException) {
+ Log.i(TAG, "RetryTestRule: RuntimeException caught, retrying the UI test")
+ unregisterAllIdlingResources()
+ exitMenu()
+ if (i == retryCount) {
+ Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
+ throw t
+ }
+ } catch (t: NullPointerException) {
+ Log.i(TAG, "RetryTestRule: NullPointerException caught, retrying the UI test")
+ unregisterAllIdlingResources()
+ exitMenu()
+ if (i == retryCount) {
+ Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
+ throw t
+ }
+ }
+ }
+ }
+ }
+
+ private inline fun statement(crossinline eval: () -> Unit): Statement {
+ return object : Statement() {
+ override fun evaluate() = eval()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/SearchDispatcher.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/SearchDispatcher.kt
new file mode 100644
index 0000000000..5d90a06a48
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/SearchDispatcher.kt
@@ -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
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/SessionLoadedIdlingResource.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/SessionLoadedIdlingResource.kt
new file mode 100644
index 0000000000..8f50e2f24f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/SessionLoadedIdlingResource.kt
@@ -0,0 +1,54 @@
+/* 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.util.Log
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.IdlingResource
+import mozilla.components.browser.state.selector.selectedTab
+import org.mozilla.fenix.FenixApplication
+import org.mozilla.fenix.helpers.Constants.TAG
+
+/**
+ * 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 {
+ Log.i(Constants.TAG, "SessionLoadedIdlingResource: Trying to return the the name of the resource: ${SessionLoadedIdlingResource::class.java.simpleName}")
+ return SessionLoadedIdlingResource::class.java.simpleName
+ }
+
+ override fun isIdleNow(): Boolean {
+ val context = ApplicationProvider.getApplicationContext()
+ 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) {
+ Log.i(TAG, "SessionLoadedIdlingResource: Trying to verify that the resource transitioned from busy to idle")
+ resourceCallback!!.onTransitionToIdle()
+ Log.i(TAG, "SessionLoadedIdlingResource: The resource transitioned to idle")
+ }
+ }
+
+ override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
+ this.resourceCallback = callback
+ Log.i(TAG, "SessionLoadedIdlingResource: Notified asynchronously that the resource is transitioning from busy to idle")
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestAssetHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestAssetHelper.kt
new file mode 100644
index 0000000000..fb90e772f3
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestAssetHelper.kt
@@ -0,0 +1,156 @@
+/* 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)
+ val waitingTimeVeryShort: Long = TimeUnit.SECONDS.toMillis(1)
+
+ 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 {
+ @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, "", "")
+ }
+
+ fun getGPCTestAsset(server: MockWebServer): TestAsset {
+ val url = server.url("pages/global_privacy_control.html").toString().toUri()!!
+
+ return TestAsset(url, "", "")
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt
new file mode 100644
index 0000000000..2dc3ce2199
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt
@@ -0,0 +1,205 @@
+/* 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.net.Uri
+import android.util.Log
+import android.view.View
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withChild
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject
+import androidx.test.uiautomator.UiObjectNotFoundException
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import 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.assertFalse
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+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.TestAssetHelper.waitingTimeVeryShort
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+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))
+ Log.i(TAG, "scrollToElementByText: Waiting for $waitingTime ms for app view to exist")
+ appView.waitForExists(waitingTime)
+ Log.i(TAG, "scrollToElementByText: Waited for $waitingTime ms for app view to exist")
+ Log.i(TAG, "scrollToElementByText: Trying to scroll text: $text into the view")
+ appView.scrollTextIntoView(text)
+ Log.i(TAG, "scrollToElementByText: Scrolled text: $text into the view")
+ return appView
+ }
+
+ fun longTapSelectItem(url: Uri) {
+ mDevice.waitNotNull(
+ Until.findObject(By.text(url.toString())),
+ waitingTime,
+ )
+ Log.i(TAG, "longTapSelectItem: Trying to long click item with $url")
+ onView(
+ allOf(
+ withId(R.id.url),
+ withText(url.toString()),
+ ),
+ ).perform(longClick())
+ Log.i(TAG, "longTapSelectItem: Long clicked item with $url")
+ }
+
+ fun restartApp(activity: HomeActivityIntentTestRule) {
+ with(activity) {
+ updateCachedSettings()
+ Log.i(TAG, "restartApp: Trying to finish the current activity")
+ finishActivity()
+ Log.i(TAG, "restartApp: Finished the current activity")
+ Log.i(TAG, "restartApp: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "restartApp: Waited for device to be idle")
+ Log.i(TAG, "restartApp: Trying to launch the activity")
+ launchActivity(null)
+ Log.i(TAG, "restartApp: Launched the activity")
+ }
+ }
+
+ fun closeApp(activity: HomeActivityIntentTestRule) {
+ Log.i(TAG, "closeApp: Trying to finish and remove the task of the current activity")
+ activity.activity.finishAndRemoveTask()
+ Log.i(TAG, "closeApp: Finished and removed the task of the current activity")
+ }
+
+ fun relaunchCleanApp(activity: HomeActivityIntentTestRule) {
+ closeApp(activity)
+ Log.i(TAG, "relaunchCleanApp: Trying to clear intents state")
+ Intents.release()
+ Log.i(TAG, "relaunchCleanApp: Cleared intents state")
+ Log.i(TAG, "relaunchCleanApp: Trying to launch the activity")
+ activity.launchActivity(null)
+ Log.i(TAG, "relaunchCleanApp: Launched the activity")
+ }
+
+ fun waitUntilObjectIsFound(resourceName: String) {
+ mDevice.waitNotNull(
+ Until.findObjects(By.res(resourceName)),
+ waitingTime,
+ )
+ }
+
+ fun clickSnackbarButton(expectedText: String) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "clickSnackbarButton: Started try #$i")
+ try {
+ Log.i(TAG, "clickSnackbarButton: Waiting for $waitingTimeShort ms for the $expectedText snackbar button to exist")
+ itemWithResIdAndText("$packageName:id/snackbar_btn", expectedText).waitForExists(waitingTimeShort)
+ Log.i(TAG, "clickSnackbarButton: Waited for $waitingTimeShort ms for the $expectedText snackbar button to exist")
+ Log.i(TAG, "clickSnackbarButton: Trying to click the $expectedText and wait for $waitingTime ms for a new window")
+ itemWithResIdAndText("$packageName:id/snackbar_btn", expectedText).clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "clickSnackbarButton: Clicked the $expectedText and waited for $waitingTime ms for a new window")
+
+ break
+ } catch (e: UiObjectNotFoundException) {
+ Log.i(TAG, "clickSnackbarButton: UiObjectNotFoundException caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ }
+ }
+ }
+ }
+
+ fun waitUntilSnackbarGone() {
+ Log.i(TAG, "waitUntilSnackbarGone: Waiting for $waitingTime ms until the snckabar is gone")
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/snackbar_layout"),
+ ).waitUntilGone(waitingTime)
+ Log.i(TAG, "waitUntilSnackbarGone: Waited for $waitingTime ms until the snckabar was gone")
+ }
+
+ fun verifySnackBarText(expectedText: String) = assertUIObjectExists(itemContainingText(expectedText))
+
+ fun verifyUrl(urlSubstring: String, resourceName: String, resId: Int) {
+ waitUntilObjectIsFound(resourceName)
+ Log.i(TAG, "verifyUrl: Waiting for $waitingTime ms for url substring: $urlSubstring to exist")
+ mDevice.findObject(UiSelector().text(urlSubstring)).waitForExists(waitingTime)
+ Log.i(TAG, "verifyUrl: Waited for $waitingTime ms for url substring: $urlSubstring to exist")
+ Log.i(TAG, "verifyUrl: Trying to verify that item with $resId contains url substring: $urlSubstring")
+ onView(withId(resId)).check(ViewAssertions.matches(withText(CoreMatchers.containsString(urlSubstring))))
+ Log.i(TAG, "verifyUrl: Verified that item with $resId contains url substring: $urlSubstring")
+ }
+
+ // exit from Menus to home screen or browser
+ fun exitMenu() {
+ val menuToolbar =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar"))
+ while (menuToolbar.waitForExists(waitingTimeShort)) {
+ Log.i(TAG, "exitMenu: Trying to press the device back button to return to the app home/browser view")
+ mDevice.pressBack()
+ Log.i(TAG, "exitMenu: Pressed the device back button to return to the app home/browser view")
+ }
+ }
+
+ fun UiDevice.waitForObjects(obj: UiObject, waitingTime: Long = TestAssetHelper.waitingTime) {
+ Log.i(TAG, "waitForObjects: Waiting for device to be idle")
+ this.waitForIdle()
+ Log.i(TAG, "waitForObjects: Waited for device to be idle")
+ Log.i(TAG, "waitForObjects: Waiting for $waitingTime ms to assert that ${obj.selector} is not null")
+ Assert.assertNotNull(obj.waitForExists(waitingTime))
+ Log.i(TAG, "waitForObjects: Waited for $waitingTime ms and asserted that ${obj.selector} is not null")
+ }
+
+ fun hasCousin(matcher: Matcher): Matcher {
+ return withParent(
+ hasSibling(
+ withChild(
+ matcher,
+ ),
+ ),
+ )
+ }
+
+ fun verifyLightThemeApplied(expected: Boolean) {
+ Log.i(TAG, "verifyLightThemeApplied: Trying to verify that that the \"Light\" theme was applied")
+ assertFalse("$TAG: Light theme not selected", expected)
+ Log.i(TAG, "verifyLightThemeApplied: Verified that that the \"Light\" theme was applied")
+ }
+
+ fun verifyDarkThemeApplied(expected: Boolean) {
+ Log.i(TAG, "verifyDarkThemeApplied: Trying to verify that that the \"Dark\" theme was applied")
+ assertTrue("$TAG: Dark theme not selected", expected)
+ Log.i(TAG, "verifyDarkThemeApplied: Verified that that the \"Dark\" theme was applied")
+ }
+
+ fun waitForAppWindowToBeUpdated() {
+ Log.i(TAG, "waitForAppWindowToBeUpdated: Waiting for $waitingTimeVeryShort ms for $packageName window to be updated")
+ mDevice.waitForWindowUpdate(packageName, waitingTimeVeryShort)
+ Log.i(TAG, "waitForAppWindowToBeUpdated: Waited for $waitingTimeVeryShort ms for $packageName window to be updated")
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestSetup.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestSetup.kt
new file mode 100644
index 0000000000..c5212f0002
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/TestSetup.kt
@@ -0,0 +1,83 @@
+/* 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.util.Log
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.store.BrowserStore
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Before
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.ui.robots.notificationShade
+import java.util.Locale
+
+/**
+ * Standard Test setup and tear down methods to run before each test.
+ * Some extra clean-up is required when we're using the org.mozilla.fenix.helpers.RetryTestRule (the instrumentation does not do that in this case).
+ *
+ */
+open class TestSetup {
+ lateinit var mockWebServer: MockWebServer
+ lateinit var browserStore: BrowserStore
+
+ @Before
+ open fun setUp() {
+ Log.i(TAG, "TestSetup: Starting the @Before setup")
+ runBlocking {
+ // Reset locale to EN-US if needed.
+ // Because of https://bugzilla.mozilla.org/show_bug.cgi?id=1812183, some items might not be updated.
+ if (Locale.getDefault() != Locale.US) {
+ AppAndSystemHelper.setSystemLocale(Locale.US)
+ }
+ // Check and clear the downloads folder, in case the tearDown method is not executed.
+ // This will only work in case of a RetryTestRule execution.
+ AppAndSystemHelper.clearDownloadsFolder()
+ // Make sure the Wifi and Mobile Data connections are on.
+ AppAndSystemHelper.setNetworkEnabled(true)
+ // Clear bookmarks left after a failed test, before a retry.
+ AppAndSystemHelper.deleteBookmarksStorage()
+ // Clear history left after a failed test, before a retry.
+ AppAndSystemHelper.deleteHistoryStorage()
+ // Clear permissions left after a failed test, before a retry.
+ AppAndSystemHelper.deletePermissionsStorage()
+ }
+
+ // Initializing this as part of class construction, below the rule would throw a NPE.
+ // So we are initializing this here instead of in all related tests.
+ Log.i(TAG, "TestSetup: Trying to initialize the browserStore instance")
+ browserStore = TestHelper.appContext.components.core.store
+ Log.i(TAG, "TestSetup: Initialized the browserStore instance")
+ // Clear pre-existing notifications.
+ notificationShade {
+ cancelAllShownNotifications()
+ }
+
+ mockWebServer = MockWebServer().apply {
+ dispatcher = AndroidAssetDispatcher()
+ }
+ try {
+ Log.i(TAG, "Try starting mockWebServer")
+ mockWebServer.start()
+ } catch (e: Exception) {
+ Log.i(TAG, "Exception caught. Re-starting mockWebServer")
+ mockWebServer.shutdown()
+ mockWebServer.start()
+ }
+ }
+
+ @After
+ open fun tearDown() {
+ Log.i(TAG, "TestSetup: Starting the @After tearDown methods.")
+ runBlocking {
+ // Reset locale to EN-US if needed.
+ // This method is only here temporarily, to set the language before a new activity is started.
+ // TODO: When https://bugzilla.mozilla.org/show_bug.cgi?id=1812183 is fixed, it should be removed.
+ if (Locale.getDefault() != Locale.US) {
+ AppAndSystemHelper.setSystemLocale(Locale.US)
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ViewVisibilityIdlingResource.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ViewVisibilityIdlingResource.kt
new file mode 100644
index 0000000000..766680cb5d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ViewVisibilityIdlingResource.kt
@@ -0,0 +1,42 @@
+/* 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.util.Log
+import android.view.View
+import androidx.test.espresso.IdlingResource
+import org.mozilla.fenix.helpers.Constants.TAG
+
+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 {
+ Log.i(TAG, "ViewVisibilityIdlingResource: Trying to return the the name of the resource: ${ViewVisibilityIdlingResource::class.java.name + ":" + view.id + ":" + expectedVisibility}")
+ return ViewVisibilityIdlingResource::class.java.name + ":" + view.id + ":" + expectedVisibility
+ }
+
+ override fun isIdleNow(): Boolean {
+ if (isIdle) return true
+
+ isIdle = view.visibility == expectedVisibility
+
+ if (isIdle) {
+ Log.i(TAG, "ViewVisibilityIdlingResource: Trying to verify that the resource transitioned from busy to idle")
+ resourceCallback?.onTransitionToIdle()
+ Log.i(TAG, "ViewVisibilityIdlingResource: The resource transitioned to idle")
+ }
+
+ return isIdle
+ }
+
+ override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
+ this.resourceCallback = callback
+ Log.i(TAG, "ViewVisibilityIdlingResource: Notified asynchronously that the resource is transitioning from busy to idle")
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/String.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/String.kt
new file mode 100644
index 0000000000..71b701679d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/String.kt
@@ -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
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/ViewInteraction.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/ViewInteraction.kt
new file mode 100644
index 0000000000..f9d8f1e541
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/ViewInteraction.kt
@@ -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,
+ ),
+ )
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/WaitNotNull.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/WaitNotNull.kt
new file mode 100644
index 0000000000..b119e34d6b
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/WaitNotNull.kt
@@ -0,0 +1,26 @@
+/* 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.util.Log
+import androidx.test.uiautomator.SearchCondition
+import androidx.test.uiautomator.UiDevice
+import org.junit.Assert.assertNotNull
+import org.mozilla.fenix.helpers.Constants.TAG
+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,
+) {
+ Log.i(TAG, "Wait not null: $searchCondition")
+ assertNotNull(wait(searchCondition, waitTime))
+ Log.i(TAG, "Found $searchCondition not null")
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt
new file mode 100644
index 0000000000..0bc68fa13d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt
@@ -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
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt
new file mode 100644
index 0000000000..aece533de8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt
@@ -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(R.id.add_ons_progress_bar)
+ } ?: return true
+
+ if (progressbar.visibility == VISIBLE) {
+ return false
+ }
+
+ return true
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/BottomSheetBehaviorStateIdlingResource.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/BottomSheetBehaviorStateIdlingResource.kt
new file mode 100644
index 0000000000..929d2e483a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/BottomSheetBehaviorStateIdlingResource.kt
@@ -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)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/NetworkConnectionIdlingResource.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/NetworkConnectionIdlingResource.kt
new file mode 100644
index 0000000000..f8c61bc514
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/NetworkConnectionIdlingResource.kt
@@ -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()
+
+ 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()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/BitmapDrawableMatcher.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/BitmapDrawableMatcher.kt
new file mode 100644
index 0000000000..7e0c3bfc1f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/BitmapDrawableMatcher.kt
@@ -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(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
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/BottomSheetBehaviorMatchers.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/BottomSheetBehaviorMatchers.kt
new file mode 100644
index 0000000000..9a081dc6f6
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/BottomSheetBehaviorMatchers.kt
@@ -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::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::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
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/RecyclerViewItemMatcher.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/RecyclerViewItemMatcher.kt
new file mode 100644
index 0000000000..469a53b3d8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/helpers/matchers/RecyclerViewItemMatcher.kt
@@ -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): Matcher? {
+ return object : BoundedMatcher(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
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/OnboardingMapperTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/OnboardingMapperTest.kt
new file mode 100644
index 0000000000..588078298e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/OnboardingMapperTest.kt
@@ -0,0 +1,490 @@
+/* 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 io.mockk.every
+import io.mockk.mockk
+import mozilla.components.service.nimbus.evalJexlSafe
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.experiments.nimbus.NimbusMessagingHelperInterface
+import org.mozilla.experiments.nimbus.StringHolder
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.nimbus.FxNimbus
+import org.mozilla.fenix.nimbus.JunoOnboarding
+import org.mozilla.fenix.nimbus.OnboardingCardData
+import org.mozilla.fenix.nimbus.OnboardingCardType
+
+class OnboardingMapperTest {
+
+ @get:Rule
+ val activityTestRule =
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ private lateinit var junoOnboardingFeature: JunoOnboarding
+ private lateinit var jexlConditions: Map
+ private lateinit var jexlHelper: NimbusMessagingHelperInterface
+ private lateinit var evalFunction: (String) -> Boolean
+
+ @Before
+ fun setup() {
+ junoOnboardingFeature = FxNimbus.features.junoOnboarding.value()
+ jexlConditions = junoOnboardingFeature.conditions
+
+ jexlHelper = mockk(relaxed = true)
+ evalFunction = { condition -> jexlHelper.evalJexlSafe(condition) }
+
+ every { evalFunction("true") } returns true
+ every { evalFunction("false") } returns false
+ }
+
+ @Test
+ fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() {
+ val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, syncPageUiData, notificationPageUiData)
+ assertEquals(
+ expected,
+ unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = true,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() {
+ val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, syncPageUiData)
+ assertEquals(
+ expected,
+ unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun pagesToDisplay_returnsSortedListOfConvertedPages_withPrivacyCaption_alwaysOnFirstPage() {
+ var result = unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = false,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ )
+ assertEquals(result[0].privacyCaption, privacyCaption)
+
+ result = unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = false,
+ showNotificationPage = true,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ )
+ assertEquals(result[0].privacyCaption, privacyCaption)
+ assertEquals(result[1].privacyCaption, null)
+
+ result = unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = true,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ )
+ assertEquals(result[0].privacyCaption, privacyCaption)
+ assertEquals(result[1].privacyCaption, null)
+ assertEquals(result[2].privacyCaption, null)
+
+ result = unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = false,
+ showNotificationPage = false,
+ showAddWidgetPage = true,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ )
+ assertEquals(result[0].privacyCaption, privacyCaption)
+ assertEquals(result[1].privacyCaption, null)
+ }
+
+ @Test
+ fun showDefaultBrowserPageFalse_showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages() {
+ val expected = listOf(addSearchWidgetPageUiDataWithPrivacyCaption, syncPageUiData)
+ assertEquals(
+ expected,
+ unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = false,
+ showNotificationPage = false,
+ showAddWidgetPage = true,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() {
+ val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, addSearchWidgetPageUiData, syncPageUiData)
+ assertEquals(
+ expected,
+ unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = true,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() {
+ val expected = listOf(
+ defaultBrowserPageUiDataWithPrivacyCaption,
+ addSearchWidgetPageUiData,
+ syncPageUiData,
+ notificationPageUiData,
+ )
+ assertEquals(
+ expected,
+ unsortedAllKnownCardData.toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = true,
+ showAddWidgetPage = true,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun cardConditionsMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
+ val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
+ val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
+
+ assertEquals(
+ expected,
+ listOf(defaultBrowserCardData).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun noJexlConditionsAndNoCardConditions_shouldDisplayCard_returnsNoPage() {
+ val jexlConditions = mapOf()
+ val expected = emptyList()
+
+ assertEquals(
+ expected,
+ listOf(addSearchWidgetCardDataNoConditions).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun noJexlConditions_shouldDisplayCard_returnsNoPage() {
+ val jexlConditions = mapOf()
+ val expected = emptyList()
+
+ assertEquals(
+ expected,
+ listOf(defaultBrowserCardData).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun prerequisitesMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
+ val jexlConditions = mapOf("ALWAYS" to "true")
+ val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
+
+ assertEquals(
+ expected,
+ listOf(defaultBrowserCardData).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun prerequisitesDontMatchJexlConditions_shouldDisplayCard_returnsNoPage() {
+ val jexlConditions = mapOf("NEVER" to "false")
+ val expected = emptyList()
+
+ assertEquals(
+ expected,
+ listOf(defaultBrowserCardData).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun noCardConditions_shouldDisplayCard_returnsNoPage() {
+ val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
+ val expected = emptyList()
+
+ assertEquals(
+ expected,
+ listOf(addSearchWidgetCardDataNoConditions).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun noDisqualifiers_shouldDisplayCard_returnsConvertedPage() {
+ val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
+ val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
+
+ assertEquals(
+ expected,
+ listOf(defaultBrowserCardDataNoDisqualifiers).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun disqualifiersMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
+ val jexlConditions = mapOf("NEVER" to "false")
+ val expected = listOf(syncPageUiDataWithPrivacyCaption)
+
+ assertEquals(
+ expected,
+ listOf(syncCardData).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun disqualifiersDontMatchJexlConditions_shouldDisplayCard_returnsNoPage() {
+ val jexlConditions = mapOf("NEVER" to "false")
+ val expected = listOf()
+
+ assertEquals(
+ expected,
+ listOf(notificationCardData).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+
+ @Test
+ fun noPrerequisites_shouldDisplayCard_returnsConvertedPage() {
+ val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
+ val expected = listOf(syncPageUiDataWithPrivacyCaption)
+
+ assertEquals(
+ expected,
+ listOf(syncCardData).toPageUiData(
+ privacyCaption = privacyCaption,
+ showDefaultBrowserPage = true,
+ showNotificationPage = false,
+ showAddWidgetPage = false,
+ jexlConditions = jexlConditions,
+ func = evalFunction,
+ ),
+ )
+ }
+}
+val privacyCaption: Caption = mockk(relaxed = true)
+
+private val defaultBrowserPageUiDataWithPrivacyCaption = OnboardingPageUiData(
+ type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
+ imageRes = R.drawable.ic_onboarding_welcome,
+ title = "default browser title",
+ description = "default browser body",
+ primaryButtonLabel = "default browser primary button text",
+ secondaryButtonLabel = "default browser secondary button text",
+ privacyCaption = privacyCaption,
+)
+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",
+ primaryButtonLabel = "add search widget primary button text",
+ secondaryButtonLabel = "add search widget secondary button text",
+ privacyCaption = null,
+)
+private val addSearchWidgetPageUiDataWithPrivacyCaption = OnboardingPageUiData(
+ type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
+ imageRes = R.drawable.ic_onboarding_search_widget,
+ title = "add search widget title",
+ description = "add search widget body",
+ primaryButtonLabel = "add search widget primary button text",
+ secondaryButtonLabel = "add search widget secondary button text",
+ privacyCaption = privacyCaption,
+)
+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",
+ privacyCaption = null,
+)
+private val syncPageUiDataWithPrivacyCaption = 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",
+ privacyCaption = privacyCaption,
+)
+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",
+ privacyCaption = null,
+)
+
+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"),
+ primaryButtonLabel = StringHolder(null, "default browser primary button text"),
+ secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
+ ordering = 10,
+ prerequisites = listOf("ALWAYS"),
+ disqualifiers = listOf("NEVER"),
+)
+
+private val defaultBrowserCardDataNoDisqualifiers = OnboardingCardData(
+ cardType = OnboardingCardType.DEFAULT_BROWSER,
+ imageRes = R.drawable.ic_onboarding_welcome,
+ title = StringHolder(null, "default browser title"),
+ body = StringHolder(null, "default browser body"),
+ primaryButtonLabel = StringHolder(null, "default browser primary button text"),
+ secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
+ ordering = 10,
+ prerequisites = listOf("ALWAYS"),
+ disqualifiers = listOf(),
+)
+
+private val addSearchWidgetCardDataNoConditions = 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"),
+ primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
+ secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
+ ordering = 15,
+ prerequisites = listOf(),
+ disqualifiers = listOf(),
+)
+
+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"),
+ 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,
+ prerequisites = listOf(),
+ disqualifiers = listOf("NEVER"),
+)
+
+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,
+ prerequisites = listOf(),
+ disqualifiers = listOf("NEVER", "OTHER"),
+)
+
+private val unsortedAllKnownCardData = listOf(
+ syncCardData,
+ notificationCardData,
+ defaultBrowserCardData,
+ addSearchWidgetCardData,
+)
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/perf/SampleBenchmark.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/perf/SampleBenchmark.kt
new file mode 100644
index 0000000000..f0517c279a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/perf/SampleBenchmark.kt
@@ -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++
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt
new file mode 100644
index 0000000000..1d17b8e040
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt
@@ -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(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.
+
+"""
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/ComposeMenuScreenShotTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/ComposeMenuScreenShotTest.kt
new file mode 100644
index 0000000000..6111b39dd4
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/ComposeMenuScreenShotTest.kt
@@ -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")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt
new file mode 100644
index 0000000000..657fadd594
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@file:Suppress("DEPRECATION")
+
+package org.mozilla.fenix.screenshots
+
+import android.os.SystemClock
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import androidx.test.uiautomator.UiDevice
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.ui.robots.homeScreen
+import tools.fastlane.screengrab.Screengrab
+import tools.fastlane.screengrab.locale.LocaleTestRule
+
+class DefaultHomeScreenTest : ScreenshotTest() {
+ private lateinit var mDevice: UiDevice
+
+ @Rule @JvmField
+ val localeTestRule = LocaleTestRule()
+
+ @get:Rule
+ var mActivityTestRule: ActivityTestRule = HomeActivityTestRule()
+
+ @Before
+ fun setUp() {
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ }
+
+ @After
+ fun tearDown() {
+ mActivityTestRule.getActivity().finishAndRemoveTask()
+ }
+
+ @Test
+ fun showDefaultHomeScreen() {
+ homeScreen {
+ verifyHomeScreen()
+ Screengrab.screenshot("HomeScreenRobot_home-screen-scroll")
+ TestAssetHelper.waitingTime
+ }
+ }
+
+ @Test
+ fun privateBrowsingTest() {
+ homeScreen {
+ SystemClock.sleep(TestAssetHelper.waitingTimeShort)
+ Screengrab.screenshot("HomeScreenRobot_home-screen")
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ clickPrivateModeScreenshotsSwitch()
+ }
+ // To get private screenshot,
+ // dismiss onboarding going to settings and back
+ mDevice.pressBack()
+ mDevice.pressBack()
+ homeScreen {
+ togglePrivateBrowsingModeOnOff()
+ Screengrab.screenshot("HomeScreenRobot_private-browsing-menu")
+ togglePrivateBrowsingModeOnOff()
+ Screengrab.screenshot("HomeScreenRobot_after-onboarding")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt
new file mode 100644
index 0000000000..9480ef5cab
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt
@@ -0,0 +1,236 @@
+/* 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.test.espresso.Espresso.onView
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AndroidAssetDispatcher
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+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 MenuScreenShotTest : ScreenshotTest() {
+ private lateinit var mockWebServer: MockWebServer
+ private lateinit var mDevice: UiDevice
+
+ @Rule
+ @JvmField
+ val localeTestRule = LocaleTestRule()
+
+ @get:Rule
+ var mActivityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
+
+ @Before
+ fun setUp() {
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ mockWebServer = MockWebServer().apply {
+ dispatcher = AndroidAssetDispatcher()
+ start()
+ }
+ }
+
+ @After
+ fun tearDown() {
+ mActivityTestRule.getActivity().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")
+ }.openTabDrawer {
+ TestAssetHelper.waitingTime
+ Screengrab.screenshot("TabDrawerRobot_one-tab-open")
+ }.openTabsListThreeDotMenu {
+ 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 {
+ }.openTabDrawer {
+ Screengrab.screenshot("TabDrawerRobot_tab-drawer-with-tabs")
+ closeTab()
+ TestAssetHelper.waitingTime
+ Screengrab.screenshot("TabDrawerRobot_remove-tab")
+ }
+ }
+
+ @Test
+ fun saveLoginPromptTest() {
+ val saveLoginTest =
+ TestAssetHelper.getSaveLoginAsset(mockWebServer)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(saveLoginTest.url) {
+ verifySaveLoginPromptIsShownNotSave()
+ SystemClock.sleep(TestAssetHelper.waitingTimeShort)
+ Screengrab.screenshot("save-login-prompt")
+ }
+ }
+}
+
+fun openHistoryThreeDotMenu() = onView(withText(R.string.library_history)).click()
+
+fun openBookmarksThreeDotMenu() = onView(withText(R.string.library_bookmarks)).click()
+
+fun editBookmarkFolder() = onView(withText(R.string.bookmark_menu_edit_button)).click()
+
+fun deleteBookmarkFolder() = onView(withText(R.string.bookmark_menu_delete_button)).click()
+
+fun tapOnTabCounter() = onView(withId(R.id.counter_text)).click()
+
+fun settingsAccountPreferences() = onView(withText(R.string.preferences_sync_2)).click()
+
+fun settingsSearch() = onView(withText(R.string.preferences_search)).click()
+
+fun settingsTheme() = onView(withText(R.string.preferences_customize)).click()
+
+fun settingsAccessibility() = onView(withText(R.string.preferences_accessibility)).click()
+
+fun settingDefaultBrowser() = onView(withText(R.string.preferences_set_as_default_browser)).click()
+
+fun settingsToolbar() = onView(withText(R.string.preferences_toolbar)).click()
+
+fun settingsTP() = onView(withText(R.string.preference_enhanced_tracking_protection)).click()
+
+fun settingsAddToHomeScreen() = onView(withText(R.string.preferences_add_private_browsing_shortcut)).click()
+
+fun settingsRemoveData() = onView(withText(R.string.preferences_delete_browsing_data)).click()
+
+fun settingsTelemetry() = onView(withText(R.string.preferences_data_collection)).click()
+
+fun loginsAndPassword() = onView(withText(R.string.preferences_passwords_logins_and_passwords)).click()
+
+fun addOns() = onView(withText(R.string.preferences_extensions)).click()
+
+fun settingsLanguage() = onView(withText(R.string.preferences_language)).click()
+
+fun verifySaveLoginPromptIsShownNotSave() {
+ mDevice.waitNotNull(Until.findObjects(By.text("test@example.com")), TestAssetHelper.waitingTime)
+ val submitButton = mDevice.findObject(By.res("submit"))
+ submitButton.clickAndWait(Until.newWindow(), TestAssetHelper.waitingTime)
+}
+
+fun clickAddFolderButtonUsingId() = onView(withId(R.id.add_bookmark_folder)).click()
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/ScreenshotTest.java b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/ScreenshotTest.java
new file mode 100644
index 0000000000..0af7fbf728
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/screenshots/ScreenshotTest.java
@@ -0,0 +1,73 @@
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ package org.mozilla.fenix.screenshots;
+
+ import android.Manifest;
+ import android.app.Instrumentation;
+ import android.content.Context;
+ import androidx.annotation.StringRes;
+ import androidx.test.platform.app.InstrumentationRegistry;
+ import androidx.test.rule.GrantPermissionRule;
+ import androidx.test.uiautomator.UiDevice;
+
+ import org.junit.Before;
+ import org.junit.Rule;
+ import org.junit.rules.TestRule;
+ import org.junit.rules.TestWatcher;
+ import org.junit.runner.Description;
+
+ import tools.fastlane.screengrab.Screengrab;
+ import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy;
+
+ /**
+ * Base class for tests that take screenshots.
+ */
+ public abstract class ScreenshotTest {
+
+ private Context targetContext;
+
+ UiDevice device;
+
+ @Rule
+ public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+
+ @Rule
+ public TestRule screenshotOnFailureRule = new TestWatcher() {
+ @Override
+ protected void failed(Throwable e, Description description) {
+ // On error take a screenshot so that we can debug it easily
+ Screengrab.screenshot("FAILURE-" + getScreenshotName(description));
+ }
+
+ private String getScreenshotName(Description description) {
+ return description.getClassName().replace(".", "-")
+ + "_"
+ + description.getMethodName().replace(".", "-");
+ }
+ };
+
+ @Before
+ public void setUpScreenshots() {
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ targetContext = instrumentation.getTargetContext();
+ device = UiDevice.getInstance(instrumentation);
+
+ // Use this to switch between default strategy and HostScreencap strategy
+ Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy());
+ }
+
+ String getString(@StringRes int resourceId) {
+ return targetContext.getString(resourceId).trim();
+ }
+
+ String getString(@StringRes int resourceId, Object... formatArgs) {
+ return targetContext.getString(resourceId, formatArgs).trim();
+ }
+
+ public void takeScreenshotsAfterWait(String filename, int waitingTime) throws InterruptedException {
+ Thread.sleep(waitingTime);
+ Screengrab.screenshot(filename);
+ }
+ }
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/Pipfile b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/Pipfile
new file mode 100644
index 0000000000..8fc29845b8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/Pipfile
@@ -0,0 +1,22 @@
+[[source]]
+url = "https://pypi.python.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+fxapom = "*"
+mozdownload = "*"
+mozinstall = "*"
+mozprofile = "*"
+mozrunner = "*"
+mozversion = "*"
+pytest = "*"
+pytest-fxa = "*"
+pytest-html = "*"
+pytest-metadata = "*"
+requests = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.10"
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/Pipfile.lock b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/Pipfile.lock
new file mode 100644
index 0000000000..5f780ffb4b
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/Pipfile.lock
@@ -0,0 +1,726 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "cf743d605ee37525af06865c7961ee99f1720fa9d6352eedc4bbfa5fa78f75ef"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.10"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.python.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "attrs": {
+ "hashes": [
+ "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
+ "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==23.1.0"
+ },
+ "blessed": {
+ "hashes": [
+ "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058",
+ "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"
+ ],
+ "markers": "python_version >= '2.7'",
+ "version": "==1.20.0"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
+ "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2023.7.22"
+ },
+ "cffi": {
+ "hashes": [
+ "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc",
+ "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a",
+ "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417",
+ "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab",
+ "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520",
+ "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36",
+ "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743",
+ "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8",
+ "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed",
+ "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684",
+ "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56",
+ "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324",
+ "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d",
+ "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235",
+ "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e",
+ "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088",
+ "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000",
+ "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7",
+ "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e",
+ "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673",
+ "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c",
+ "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe",
+ "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2",
+ "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098",
+ "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8",
+ "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a",
+ "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0",
+ "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b",
+ "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896",
+ "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e",
+ "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9",
+ "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2",
+ "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b",
+ "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6",
+ "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404",
+ "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f",
+ "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0",
+ "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4",
+ "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc",
+ "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936",
+ "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba",
+ "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872",
+ "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb",
+ "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614",
+ "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1",
+ "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d",
+ "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969",
+ "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b",
+ "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4",
+ "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627",
+ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956",
+ "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"
+ ],
+ "markers": "platform_python_implementation != 'PyPy'",
+ "version": "==1.16.0"
+ },
+ "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"
+ },
+ "cryptography": {
+ "hashes": [
+ "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b",
+ "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce",
+ "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88",
+ "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7",
+ "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20",
+ "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9",
+ "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff",
+ "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1",
+ "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764",
+ "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b",
+ "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298",
+ "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1",
+ "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824",
+ "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257",
+ "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a",
+ "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129",
+ "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb",
+ "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929",
+ "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854",
+ "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52",
+ "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923",
+ "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885",
+ "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0",
+ "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd",
+ "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2",
+ "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18",
+ "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b",
+ "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992",
+ "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74",
+ "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660",
+ "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925",
+ "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==42.0.4"
+ },
+ "distro": {
+ "hashes": [
+ "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8",
+ "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==1.8.0"
+ },
+ "exceptiongroup": {
+ "hashes": [
+ "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
+ "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
+ ],
+ "markers": "python_version < '3.11'",
+ "version": "==1.1.3"
+ },
+ "fxapom": {
+ "hashes": [
+ "sha256:56fdff0a0f0ea58831337e3a859971f98c59fd028ebc14baa8e37ae08a40efa0",
+ "sha256:5fb902afaaa9d9b82b5d1d54b9e19f1f4c9be128deb3b0e0ac82a9303f76000f"
+ ],
+ "index": "pypi",
+ "version": "==1.10.2"
+ },
+ "h11": {
+ "hashes": [
+ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
+ "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==0.14.0"
+ },
+ "hawkauthlib": {
+ "hashes": [
+ "sha256:935878d3a75832aa76f78ddee13491f1466cbd69a8e7e4248902763cf9953ba9",
+ "sha256:effd64a2572e3c0d9090b55ad2180b36ad50e7760bea225cb6ce2248f421510d"
+ ],
+ "version": "==2.0.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"
+ },
+ "mozdevice": {
+ "hashes": [
+ "sha256:0f33260e74d734d5f3ac277b28064bac0f5bd1ecce111fe1a73ca61eb3e4b524",
+ "sha256:ff0e0d4c618a595c26d9d1a9a071db1f5c5383b4c5cb9e7889019bd13885825b"
+ ],
+ "version": "==4.1.1"
+ },
+ "mozdownload": {
+ "hashes": [
+ "sha256:1664b0bf48eab69fafa73d3fc4dc19f4c66dfc21045fab3ca76a29b3eeb31702",
+ "sha256:d861936c2efcc7620858a097907bfaba5d6d114867b6633e4301da9263627819"
+ ],
+ "index": "pypi",
+ "version": "==1.26.0"
+ },
+ "mozfile": {
+ "hashes": [
+ "sha256:3b0afcda2fa8b802ef657df80a56f21619008f61fcc14b756124028d7b7adf5c",
+ "sha256:92ca1a786abbdf5e6a7aada62d3a4e28f441ef069c7623223add45268e53c789"
+ ],
+ "version": "==3.0.0"
+ },
+ "mozinfo": {
+ "hashes": [
+ "sha256:5d2b8a5f1b362692f221e33eb3ff47454a580db1a1384614cdc637b31131b438",
+ "sha256:90e0cfb377fc2cc3fad023d38c1f6d60a9135400ff5684a04abf79ca5cc3c521"
+ ],
+ "version": "==1.2.3"
+ },
+ "mozinstall": {
+ "hashes": [
+ "sha256:0b14000a88d6a45c37b877eedf897655f665e89410ca629dd500415406ed465e",
+ "sha256:ec364cfefd3435fb155edd48be9e71819834e4dcacc6c3294c7f2452e200095b"
+ ],
+ "index": "pypi",
+ "version": "==2.0.1"
+ },
+ "mozlog": {
+ "hashes": [
+ "sha256:26e5e9586afe2d6359a3d75aa6ea25aa2904d0062d0a158418682e44458d98e9"
+ ],
+ "version": "==8.0.0"
+ },
+ "mozprocess": {
+ "hashes": [
+ "sha256:7dc38ec3c11693e9944ade1558392c04f37fd8df68d3ec7a20372dfe96b2e5bb",
+ "sha256:ae343fabb72840195278b73ad426fb04d35d66b15a664f5c47221f4e4f0842e5"
+ ],
+ "version": "==1.3.1"
+ },
+ "mozprofile": {
+ "hashes": [
+ "sha256:5b93462c16ba7c6cd7010035765627d565c2adc7c58ac8bf82a3b1b2c14f0daa",
+ "sha256:9f77840583432bc5605375b760a6c420328f2dc95c3e8950245e4b01d65da67e"
+ ],
+ "index": "pypi",
+ "version": "==2.5.0"
+ },
+ "mozrunner": {
+ "hashes": [
+ "sha256:35a7d2cf3abee1d8651e92f870f75159605810ba8ea442defae41a5eec462c29",
+ "sha256:4ee4d44123c1daa7f3648e8b3b0e3a8c1712b3e1ea254d9a4bf80295ea795d41"
+ ],
+ "index": "pypi",
+ "version": "==8.2.1"
+ },
+ "mozterm": {
+ "hashes": [
+ "sha256:b1e91acec188de07c704dbb7b0100a7be5c1e06567b3beb67f6ea11d00a483a4",
+ "sha256:f5eafa25c23d391e2a2bb1dd45ee928fc9e3c811977a3856b5a5a0778011053c"
+ ],
+ "version": "==1.0.0"
+ },
+ "mozversion": {
+ "hashes": [
+ "sha256:42f2ce3c23e1835071d1d6f52ebec524d0bfcc036d043cfa854439f6d1dacff0",
+ "sha256:fe8e90ba54e8172113400ea10ea984827638ddd7c8329ca74426fc55c6886159"
+ ],
+ "index": "pypi",
+ "version": "==2.3.0"
+ },
+ "outcome": {
+ "hashes": [
+ "sha256:588ef4dc10b64e8df160d8d1310c44e1927129a66d6d2ef86845cef512c5f24c",
+ "sha256:7b688fd82db72f4b0bc9e883a00359d4d4179cd97d27f09c9644d0c842ba7786"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.3.0"
+ },
+ "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"
+ },
+ "progressbar2": {
+ "hashes": [
+ "sha256:1393922fcb64598944ad457569fbeb4b3ac189ef50b5adb9cef3284e87e394ce",
+ "sha256:1a8e201211f99a85df55f720b3b6da7fb5c8cdef56792c4547205be2de5ea606"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==4.2.0"
+ },
+ "py": {
+ "hashes": [
+ "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
+ "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==1.11.0"
+ },
+ "pybrowserid": {
+ "hashes": [
+ "sha256:6c227669e87cc25796ae76f6a0ef65025528c8ad82d352679fa9a3e5663a71e3",
+ "sha256:8e237d6a2bc9ead849a4472a84d3e6a9309bec99cf8e10d36213710dda8df8ca"
+ ],
+ "version": "==0.14.0"
+ },
+ "pycparser": {
+ "hashes": [
+ "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+ "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+ ],
+ "version": "==2.21"
+ },
+ "pyfxa": {
+ "hashes": [
+ "sha256:6c85cd08cf05f7138dee1cf2a8a1d68fd428b7b5ad488917c70a2a763d651cdb"
+ ],
+ "version": "==0.7.7"
+ },
+ "pyjwt": {
+ "hashes": [
+ "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de",
+ "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.8.0"
+ },
+ "pypom": {
+ "hashes": [
+ "sha256:5da52cf447e62f43a0cfa47dfe52eb822eff07b2fdad759f930d1d227c15220b",
+ "sha256:8b4dc6d1a24580298bf5ad8ad6c586f33b73c326c10a4419f83aee1abb20077d"
+ ],
+ "version": "==2.2.4"
+ },
+ "pysocks": {
+ "hashes": [
+ "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
+ "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
+ "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
+ ],
+ "version": "==1.7.1"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c",
+ "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==7.1.2"
+ },
+ "pytest-fxa": {
+ "hashes": [
+ "sha256:778dfdb019f1e0af8744704fe5f7ac5c08fd5d45ff054023b0a18d5f99d737f1",
+ "sha256:b75967e74e9b2f3ffa5558421fdf61c7fff5948fc9d7e357e7147c682988ecc1"
+ ],
+ "index": "pypi",
+ "version": "==1.4.0"
+ },
+ "pytest-html": {
+ "hashes": [
+ "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3",
+ "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.6'",
+ "version": "==3.1.1"
+ },
+ "pytest-metadata": {
+ "hashes": [
+ "sha256:39261ee0086f17649b180baf2a8633e1922a4c4b6fcc28a2de7d8127a82541bf",
+ "sha256:fcd2f416f15be295943527b3c8ba16a44ae5a7141939c90c3dc5ce9d167cf2a5"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "version": "==2.0.2"
+ },
+ "python-utils": {
+ "hashes": [
+ "sha256:ec3a672465efb6c673845a43afcfafaa23d2594c24324a40ec18a0c59478dc0b",
+ "sha256:efdf31c8154667d7dc0317547c8e6d3b506c5d4b6e360e0c89662306262fc0ab"
+ ],
+ "markers": "python_version >= '3.9'",
+ "version": "==3.8.1"
+ },
+ "redo": {
+ "hashes": [
+ "sha256:36784bf8ae766e14f9db0e377ccfa02835d648321d2007b6ae0bf4fd612c0f94",
+ "sha256:71161cb0e928d824092a5f16203939bbc0867ce4c4685db263cf22c3ae7634a8"
+ ],
+ "version": "==2.0.3"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.31.0"
+ },
+ "selenium": {
+ "hashes": [
+ "sha256:0d14b0d9842366f38fb5f8f842cf7c042bcfa062affc6a0a86e4d634bdd0fe54",
+ "sha256:be9824a9354a7fe288e3fad9ceb6a9c65ddc7c44545d23ad0ebf4ce202b19893"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.14.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
+ "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==68.2.2"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.16.0"
+ },
+ "sniffio": {
+ "hashes": [
+ "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
+ "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==1.3.0"
+ },
+ "sortedcontainers": {
+ "hashes": [
+ "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
+ "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
+ ],
+ "version": "==2.4.0"
+ },
+ "tomli": {
+ "hashes": [
+ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+ "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.1"
+ },
+ "treeherder-client": {
+ "hashes": [
+ "sha256:4020809424384574277232023c78bcee436ec5474020b4430b4770f0ddd8bba3",
+ "sha256:db25150480d0501c79b72966899e5c901a5a625e12739389f6bee03273e1d002"
+ ],
+ "version": "==5.0.0"
+ },
+ "trio": {
+ "hashes": [
+ "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3",
+ "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==0.22.2"
+ },
+ "trio-websocket": {
+ "hashes": [
+ "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f",
+ "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==0.11.1"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
+ "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.8.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
+ "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.7"
+ },
+ "wcwidth": {
+ "hashes": [
+ "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704",
+ "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4"
+ ],
+ "version": "==0.2.8"
+ },
+ "webob": {
+ "hashes": [
+ "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b",
+ "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.8.7"
+ },
+ "wsproto": {
+ "hashes": [
+ "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
+ "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==1.2.0"
+ },
+ "zope.component": {
+ "hashes": [
+ "sha256:96d0a04db39643caf2dfaec152340f3e914df1dc3fa32fbb913782620dc6c3c6",
+ "sha256:9a0a0472ad201b94b4fe6741ce9ac2c30b8bb22c516077bf03692dec4dfb6906"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==6.0"
+ },
+ "zope.event": {
+ "hashes": [
+ "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26",
+ "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==5.0"
+ },
+ "zope.hookable": {
+ "hashes": [
+ "sha256:070776c9f36b99fb0df5af2a762a4d4f77e568df36637797e2e8a41c9d8d290d",
+ "sha256:12959a3d70c35a6b835e69d9f70008d3a31e324d2f2d13536c8533f648fa8a96",
+ "sha256:1668993d40a7cfdc867843dd5725929e7f83a5b0c195c709af1daef8274f43cb",
+ "sha256:1a97f4a46d87ee726d48f3058062e2a1570f136ba9aa788e9c0bcdd5e511609d",
+ "sha256:20936873c8b17f903bc8b63ca13ec6c492665b48067988e4359ddd5d1c5b6f2f",
+ "sha256:2968b37457079678a9d1bd9ef09ff1a224d4234e02120792a9e4e00117193df3",
+ "sha256:2d7bfcb11356e4dbb3e24943f0055819ff264dada4dc0b68ca012e5a1ff5b474",
+ "sha256:2d7c782bbfed7aa4846af2a67269718563daa904b33077d97665e5644b08ce2b",
+ "sha256:351cc91c0bc4c9a6d537c033179be22b613e3a60be42ba08f863490c32f736cf",
+ "sha256:3875bfb6d113ecb41c07dee67be16f5a0bbbae13199b9979e2bbeec97d97ec4b",
+ "sha256:4d3200d955c4182223f04593fef4add9771d4156d4ba6f034e65396f3b132139",
+ "sha256:55a0a9d62ea907861fd79ae39e86f1d9e755064543e46c5430aa586c1b5a4854",
+ "sha256:5efffb4417604561ff0bae5c80ad3aa2ecd2145c5a8c3a4b0a4a1f55017e95a2",
+ "sha256:6cd064359ba8c356b1bdb6c84df028ce2f6402f3703a930c4e1bab25d3ff7fff",
+ "sha256:6d5f83e626caa7ed2814609b446dcc6a3abb19db729bc67671c3eef2265006fd",
+ "sha256:6f4d8b99c1d52e3da1b122e42e7c07eb02f6468cd315f0b6811f426586b7aa8c",
+ "sha256:6ff30e7b24859974f2ff3f00b4564c4c8692730690c4c46f0019ef9b42b1f795",
+ "sha256:7761c5fdf97a274ce8576002a2444ff45645327179ee1bafde5d5d743d0d3556",
+ "sha256:78e4953334592c42aefa3e74f74d4c5b168a70d2c2d8cd945eb1a2f442eebee5",
+ "sha256:7c5a8204992fe677bffa0e5e190cb031aef74994c658a0402a338eed7b58fe8d",
+ "sha256:7ca296b1fb0c5f69e8c0e5a90a5a953e456931016fd1f8c513b3baa3751b0640",
+ "sha256:86bc17b6b3d1d9274168318cf171d509cbe6c8a8bdd8be0282291dac4a768de0",
+ "sha256:968f196347fa1bd9ffc15e1d1c9d250f46137d36b75bdd2a482c51c5fc314402",
+ "sha256:aaac43ac9bf9359db5170627f645c6442e9cf74414f8299ee217e4cfb259bc5c",
+ "sha256:ad48a4db8d12701759b93f3cec55aff9f53626dff12ec415144c2d0ee719b965",
+ "sha256:b99ddae52522dce614a0323812df944b1835d97f254f81c46b33c3bcf82dadf5",
+ "sha256:ba0e86642d5b33b5edf39d28da26ed34545780a2720aa79e6dda94402c3fc457",
+ "sha256:c0db442d2e78d5ea1afa5f1c2537bf7201155ec8963abc8d0f3b9257b52caffb",
+ "sha256:c2cf62d40f689d4bfbe733e3ed41ed2b557d011d9050185abb2bc3e96130677d",
+ "sha256:cd6fb03c174a4e20f4faec9ff22bace922bb59adb44078aebec862605bbcee92",
+ "sha256:e21dc34ba2453d798cf3cab92efb4994e55659c19a1f77d4cf0c2a0067e78583",
+ "sha256:ee1e32f54db69abfb6c7e227e65452d2b92e1cefae93a51106419ec623a845ff",
+ "sha256:ee7ff109b2b4793137b6bd82ddc3516cbd643e67813e11e31e0bf613b387d2ec",
+ "sha256:f2eeba6e2fd69e9e72a003edcceed7ce4614ad1c1e866bf168c92540c3658343",
+ "sha256:f58a129a63289c44ba84ae951019f8a60d34c4d948350be7fa2abda5106f8498",
+ "sha256:ff5ee2df0dc3ccc524772e00d5a1991c3b8d942cc12fd503a7bf9dc35a040779"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==6.0"
+ },
+ "zope.interface": {
+ "hashes": [
+ "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff",
+ "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c",
+ "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac",
+ "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f",
+ "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d",
+ "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309",
+ "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736",
+ "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179",
+ "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb",
+ "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941",
+ "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d",
+ "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92",
+ "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b",
+ "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41",
+ "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f",
+ "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3",
+ "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d",
+ "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8",
+ "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3",
+ "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1",
+ "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1",
+ "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40",
+ "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d",
+ "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1",
+ "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605",
+ "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7",
+ "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd",
+ "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43",
+ "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0",
+ "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b",
+ "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379",
+ "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a",
+ "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83",
+ "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56",
+ "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9",
+ "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==6.1"
+ }
+ },
+ "develop": {}
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt
new file mode 100644
index 0000000000..4a374b141b
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt
@@ -0,0 +1,221 @@
+/* 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.syncintegration
+
+import android.os.SystemClock.sleep
+import android.widget.EditText
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import okhttp3.mockwebserver.MockWebServer
+import org.hamcrest.Matchers.allOf
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AndroidAssetDispatcher
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.ui.robots.accountSettings
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.settingsSubMenuLoginsAndPassword
+
+@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
+class SyncIntegrationTest {
+ private lateinit var mDevice: UiDevice
+ private lateinit var mockWebServer: MockWebServer
+
+ @get:Rule
+ val activityTestRule = HomeActivityTestRule()
+
+ @Before
+ fun setUp() {
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ mockWebServer = MockWebServer().apply {
+ dispatcher = AndroidAssetDispatcher()
+ start()
+ }
+ }
+
+ @After
+ fun tearDown() {
+ mockWebServer.shutdown()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/352905
+ // History item Desktop -> Fenix
+ @Test
+ fun syncHistoryBetweenMobileAndDesktopTest() {
+ signInFxSync()
+ tapReturnToPreviousApp()
+ // Let's wait until homescreen is shown to go to three dot menu
+ TestAssetHelper.waitingTime
+ mDevice.waitNotNull(Until.findObjects(By.res("org.mozilla.fenix.debug:id/counter_root")))
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }
+ historyAfterSyncIsShown()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/330146
+ // Bookmark item Desktop -> Fenix
+ @Test
+ fun syncBookmarksTest() {
+ signInFxSync()
+ tapReturnToPreviousApp()
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks { }
+ bookmarkAfterSyncIsShown()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243353
+ @SmokeTest
+ @Test
+ fun manageAccountSettingsTest() {
+ signInFxSync()
+ mDevice.waitNotNull(Until.findObjects(By.text("Account")), TestAssetHelper.waitingTime)
+
+ goToAccountSettings()
+ // This function to be added to the robot once the status of checkboxes can be checked
+ // currently is not possible to select each one (History/Bookmark) and verify its status
+ // verifyCheckBoxesSelected()
+ // Then select/unselect each one and verify again that its status is correct
+ // See issue #6544
+ accountSettings {
+ verifyBookmarksCheckbox()
+ verifyHistoryCheckbox()
+ verifySignOutButton()
+ verifyDeviceName()
+ }.disconnectAccount {
+ mDevice.waitNotNull(Until.findObjects(By.text("Settings")), TestAssetHelper.waitingTime)
+ verifySettingsView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/466387
+ // Login item Desktop -> Fenix
+ @Test
+ fun synLoginsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSyncLogins {
+ // Tap to sign in from Logins menu
+ tapOnUseEmailToSignIn()
+ typeEmail()
+ tapOnContinueButton()
+ typePassword()
+ tapOnSignIn()
+ }
+ // Automatically goes back to Logins and passwords view
+ settingsSubMenuLoginsAndPassword {
+ verifyDefaultView()
+ // Sync logings option is set to Off, no synced logins yet
+ verifyDefaultViewBeforeSyncComplete()
+ }.openSavedLogins {
+ // Discard the secure your device message
+ tapSetupLater()
+ // Check the logins synced
+ verifySavedLoginsAfterSync()
+ }.goBack {
+ // After checking the synced logins
+ // on Logins and Passwords menu the Sync passwords option is set to On
+ verifyDefaultViewAfterSync()
+ }
+ }
+
+ // Useful functions for the tests
+ fun typeEmail() {
+ val emailInput = mDevice.findObject(
+ UiSelector()
+ .instance(0)
+ .className(EditText::class.java),
+ )
+ emailInput.waitForExists(TestAssetHelper.waitingTime)
+
+ val emailAddress = javaClass.classLoader!!.getResource("email.txt").readText()
+ emailInput.setText(emailAddress)
+ }
+
+ fun tapOnContinueButton() {
+ val continueButton = mDevice.findObject(By.res("submit-btn"))
+ continueButton.clickAndWait(Until.newWindow(), TestAssetHelper.waitingTime)
+ }
+
+ fun typePassword() {
+ val passwordInput = mDevice.findObject(
+ UiSelector()
+ .instance(0)
+ .className(EditText::class.java),
+ )
+
+ val passwordValue = javaClass.classLoader!!.getResource("password.txt").readText()
+ passwordInput.setText(passwordValue)
+ }
+
+ fun tapOnSignIn() {
+ mDevice.waitNotNull(Until.findObjects(By.text("Sign in")))
+ // Let's tap on enter, sometimes depending on the device the sign in button is
+ // hidden by the keyboard
+ mDevice.pressEnter()
+ }
+
+ fun historyAfterSyncIsShown() {
+ mDevice.waitNotNull(Until.findObjects(By.text("http://www.example.com/")), TestAssetHelper.waitingTime)
+ }
+
+ fun bookmarkAfterSyncIsShown() {
+ val bookmarkEntry = mDevice.findObject(By.text("Example Domain"))
+ bookmarkEntry.isEnabled()
+ }
+
+ fun tapReturnToPreviousApp() {
+ mDevice.waitNotNull(Until.findObjects(By.text("Save")), TestAssetHelper.waitingTime)
+ mDevice.waitNotNull(Until.findObjects(By.text("Settings")), TestAssetHelper.waitingTime)
+
+ /* Wait until the Settings shows the account synced */
+ mDevice.waitNotNull(Until.findObjects(By.text("Account")), TestAssetHelper.waitingTime)
+ mDevice.waitNotNull(Until.findObjects(By.res("org.mozilla.fenix.debug:id/email")), TestAssetHelper.waitingTime)
+ TestAssetHelper.waitingTime
+ // Go to Homescreen
+ mDevice.pressBack()
+ }
+
+ fun signInFxSync() {
+ homeScreen {
+ }.openThreeDotMenu {
+ verifySettingsButton()
+ }.openSettings {}
+ settingsAccount()
+ useEmailInsteadButton()
+
+ typeEmail()
+ tapOnContinueButton()
+ typePassword()
+ sleep(TestAssetHelper.waitingTimeShort)
+ tapOnSignIn()
+ }
+
+ fun goToAccountSettings() {
+ enterAccountSettings()
+ mDevice.waitNotNull(Until.findObjects(By.text("Device name")), TestAssetHelper.waitingTime)
+ }
+}
+
+fun settingsAccount() = onView(allOf(withText("Turn on Sync"))).perform(click())
+fun useEmailInsteadButton() = onView(withId(R.id.signInEmailButton)).perform(click())
+fun enterAccountSettings() = onView(withId(R.id.email)).perform(click())
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/__init__.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/adbrun.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/adbrun.py
new file mode 100644
index 0000000000..be4244674e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/adbrun.py
@@ -0,0 +1,17 @@
+import logging
+import os
+
+logging.getLogger(__name__).addHandler(logging.NullHandler())
+
+
+class ADBrun(object):
+ binary = "adbrun"
+ logger = logging.getLogger()
+
+ def launch(self):
+ # First close sim if any then launch
+ os.system(
+ "~/Library/Android/sdk/platform-tools/adb devices | grep emulator | cut -f1 | while read line; do ~/Library/Android/sdk/platform-tools/adb -s $line emu kill; done"
+ )
+ # Then launch sim
+ os.system("sh launchSimScript.sh")
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/conftest.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/conftest.py
new file mode 100644
index 0000000000..f270b854ac
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/conftest.py
@@ -0,0 +1,192 @@
+import io
+import json
+import os
+import time
+
+import mozinstall
+import mozversion
+import pytest
+import requests
+from mozdownload import DirectScraper, FactoryScraper
+from mozprofile import Profile
+
+from .gradlewbuild import GradlewBuild
+from .tps import TPS
+
+here = os.path.dirname(__file__)
+
+
+@pytest.fixture(scope="session")
+def firefox(pytestconfig, tmpdir_factory):
+ binary = os.getenv("MOZREGRESSION_BINARY", pytestconfig.getoption("firefox"))
+ if binary is None:
+ cache_dir = str(pytestconfig.cache.makedir("firefox"))
+ scraper = FactoryScraper("daily", destination=cache_dir)
+ build_path = scraper.download()
+ install_path = str(tmpdir_factory.mktemp("firefox"))
+ install_dir = mozinstall.install(src=build_path, dest=install_path)
+ binary = mozinstall.get_binary(install_dir, "firefox")
+ version = mozversion.get_version(binary)
+ if hasattr(pytestconfig, "_metadata"):
+ pytestconfig._metadata.update(version)
+ return binary
+
+
+@pytest.fixture
+def firefox_log(pytestconfig, tmpdir):
+ firefox_log = str(tmpdir.join("firefox.log"))
+ pytestconfig._firefox_log = firefox_log
+ yield firefox_log
+
+
+@pytest.fixture(scope="session")
+def tps_addon(pytestconfig, tmpdir_factory):
+ path = pytestconfig.getoption("tps")
+ if path is not None:
+ return path
+ task_url = (
+ "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/"
+ "gecko.v2.mozilla-central.latest.firefox.addons.tps"
+ )
+ task_id = requests.get(task_url).json().get("taskId")
+ cache_dir = str(pytestconfig.cache.makedir("tps-{}".format(task_id)))
+ addon_url = (
+ "https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/"
+ "{}/artifacts/public/tps.xpi".format(task_id)
+ )
+ scraper = DirectScraper(addon_url, destination=cache_dir)
+ return scraper.download()
+
+
+@pytest.fixture
+def tps_config(fxa_account, monkeypatch):
+ monkeypatch.setenv("FXA_EMAIL", fxa_account.email)
+ monkeypatch.setenv("FXA_PASSWORD", fxa_account.password)
+
+ # Go to resources folder
+ os.chdir("../../../../..")
+ resources = r"resources"
+ resourcesDir = os.path.join(os.getcwd(), resources)
+
+ with open(os.path.join(resourcesDir, "email.txt"), "w") as f:
+ f.write(fxa_account.email)
+
+ with open(os.path.join(resourcesDir, "password.txt"), "w") as f:
+ f.write(fxa_account.password)
+
+ # Set the path where tests are
+ os.chdir("../")
+ currentDir = os.getcwd()
+ testsDir = currentDir + "/androidTest/java/org/mozilla/fenix/syncintegration"
+ os.chdir(testsDir)
+
+ yield {
+ "fx_account": {"username": fxa_account.email, "password": fxa_account.password}
+ }
+
+
+@pytest.fixture
+def tps_log(pytestconfig, tmpdir):
+ tps_log = str(tmpdir.join("tps.log"))
+ pytestconfig._tps_log = tps_log
+ yield tps_log
+
+
+@pytest.fixture
+def tps_profile(pytestconfig, tps_addon, tps_config, tps_log, fxa_urls):
+ preferences = {
+ "app.update.enabled": False,
+ "browser.dom.window.dump.enabled": True,
+ "browser.onboarding.enabled": False,
+ "browser.sessionstore.resume_from_crash": False,
+ "browser.shell.checkDefaultBrowser": False,
+ "browser.startup.homepage_override.mstone": "ignore",
+ "browser.startup.page": 0,
+ "browser.tabs.warnOnClose": False,
+ "browser.warnOnQuit": False,
+ "datareporting.policy.dataSubmissionEnabled": False,
+ # 'devtools.chrome.enabled': True,
+ # 'devtools.debugger.remote-enabled': True,
+ "engine.bookmarks.repair.enabled": False,
+ "extensions.autoDisableScopes": 10,
+ "extensions.experiments.enabled": True,
+ "extensions.update.enabled": False,
+ "extensions.update.notifyUser": False,
+ # While this line is commented prod is launched instead of stage
+ "identity.fxaccounts.autoconfig.uri": fxa_urls["content"],
+ "testing.tps.skipPingValidation": True,
+ "services.sync.firstSync": "notReady",
+ "services.sync.lastversion": "1.0",
+ "services.sync.log.appender.console": "Trace",
+ "services.sync.log.appender.dump": "Trace",
+ "services.sync.log.appender.file.level": "Trace",
+ "services.sync.log.appender.file.logOnSuccess": True,
+ "services.sync.log.logger": "Trace",
+ "services.sync.log.logger.engine": "Trace",
+ "services.sync.testing.tps": True,
+ "testing.tps.logFile": tps_log,
+ "toolkit.startup.max_resumed_crashes": -1,
+ "tps.config": json.dumps(tps_config),
+ "tps.seconds_since_epoch": int(time.time()),
+ "xpinstall.signatures.required": False,
+ }
+ profile = Profile(addons=[tps_addon], preferences=preferences)
+ pytestconfig._profile = profile.profile
+ yield profile
+
+
+@pytest.fixture
+def tps(firefox, firefox_log, monkeypatch, pytestconfig, tps_log, tps_profile):
+ yield TPS(firefox, firefox_log, tps_log, tps_profile)
+
+
+@pytest.fixture
+def gradlewbuild_log(pytestconfig, tmpdir):
+ gradlewbuild_log = str(tmpdir.join("gradlewbuild.log"))
+ pytestconfig._gradlewbuild_log = gradlewbuild_log
+ yield gradlewbuild_log
+
+
+@pytest.fixture
+def gradlewbuild(fxa_account, monkeypatch, gradlewbuild_log):
+ monkeypatch.setenv("FXA_EMAIL", fxa_account.email)
+ monkeypatch.setenv("FXA_PASSWORD", fxa_account.password)
+ yield GradlewBuild(gradlewbuild_log)
+
+
+def pytest_addoption(parser):
+ parser.addoption(
+ "--firefox",
+ help="path to firefox binary (defaults to " "downloading latest nightly build)",
+ )
+ parser.addoption(
+ "--tps",
+ help="path to tps add-on (defaults to " "downloading latest nightly build)",
+ )
+
+
+@pytest.mark.hookwrapper
+def pytest_runtest_makereport(item, call):
+ outcome = yield
+ report = outcome.get_result()
+ extra = getattr(report, "extra", [])
+ pytest_html = item.config.pluginmanager.getplugin("html")
+ profile = getattr(item.config, "_profile", None)
+ if profile is not None and os.path.exists(profile):
+ # add sync logs to HTML report
+ for root, _, files in os.walk(os.path.join(profile, "weave", "logs")):
+ for f in files:
+ path = os.path.join(root, f)
+ if pytest_html is not None:
+ with io.open(path, "r", encoding="utf8") as f:
+ extra.append(pytest_html.extras.text(f.read(), "Sync"))
+ report.sections.append(("Sync", "Log: {}".format(path)))
+ for log in ("Firefox", "TPS", "GradlewBuild"):
+ attr = "_{}_log".format(log.lower())
+ path = getattr(item.config, attr, None)
+ if path is not None and os.path.exists(path):
+ if pytest_html is not None:
+ with io.open(path, "r", encoding="utf8") as f:
+ extra.append(pytest_html.extras.text(f.read(), log))
+ report.sections.append((log, "Log: {}".format(path)))
+ report.extra = extra
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/gradlewbuild.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/gradlewbuild.py
new file mode 100644
index 0000000000..1cd65a6f45
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/gradlewbuild.py
@@ -0,0 +1,45 @@
+import logging
+import os
+import subprocess
+
+from .adbrun import ADBrun
+
+here = os.path.dirname(__file__)
+logging.getLogger(__name__).addHandler(logging.NullHandler())
+
+
+class GradlewBuild(object):
+ binary = "./gradlew"
+ logger = logging.getLogger()
+ adbrun = ADBrun()
+
+ def __init__(self, log):
+ self.log = log
+
+ def test(self, identifier):
+ self.adbrun.launch()
+
+ # Change path accordingly to go to root folder to run gradlew
+ os.chdir("../../../../../../../..")
+ cmd = (
+ "./gradlew "
+ + "app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.syncintegration.SyncIntegrationTest#{}".format(
+ identifier
+ )
+ )
+
+ self.logger.info("Running cmd: {}".format(cmd))
+
+ out = ""
+ try:
+ out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ out = e.output
+ raise
+ finally:
+ # Set the path correctly
+ testsPath = "app/src/androidTest/java/org/mozilla/fenix/syncintegration/"
+ os.chdir(testsPath)
+
+ with open(self.log, "w") as f:
+ f.write(str(out))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/launchSimScript.sh b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/launchSimScript.sh
new file mode 100755
index 0000000000..648397f0ec
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/launchSimScript.sh
@@ -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
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/pytest.ini b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/pytest.ini
new file mode 100644
index 0000000000..fda734a9f8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+addopts = --verbose --html=results/index.html
+log_cli = true
+log_cli_level = info
\ No newline at end of file
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_bookmark.js b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_bookmark.js
new file mode 100644
index 0000000000..e1210bd24e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_bookmark.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+EnableEngines(["bookmarks"]);
+
+var phases = { "phase1": "profile1" };
+
+
+// expected bookmark state
+var bookmarksCreated = {
+"mobile": [{
+ uri: "http://www.example.com/",
+ title: "Example Domain"}]
+};
+
+// sync and verify bookmarks
+Phase("phase1", [
+ [Sync],
+ [Bookmarks.add, bookmarksCreated],
+ [Sync]
+]);
\ No newline at end of file
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_bookmark_desktop.js b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_bookmark_desktop.js
new file mode 100644
index 0000000000..a6a42aa6d0
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_bookmark_desktop.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+EnableEngines(["bookmarks"]);
+
+var phases = { "phase1": "profile1" };
+
+
+// expected bookmark state
+var bookmarksExpected = {
+"mobile": [{
+ uri: "http://www.example.com/",
+ title: "Example Domain"}]
+};
+
+// sync and verify bookmarks
+Phase("phase1", [
+ [Sync],
+ [Bookmarks.verify, bookmarksExpected],
+]);
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_history.js b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_history.js
new file mode 100644
index 0000000000..8b915945e2
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_history.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+EnableEngines(["history"]);
+
+var phases = { "phase1": "profile1" };
+
+
+// expected history state
+var historyCreated = [
+ { uri: "http://www.example.com/",
+ visits: [
+ { type: 1 ,
+ date: 0
+ },
+ { type: 2,
+ date: -1
+ }
+ ]
+ }
+];
+
+// sync and verify history
+Phase("phase1", [
+ [Sync],
+ [History.add, historyCreated],
+ [Sync]
+]);
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_history_desktop.js b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_history_desktop.js
new file mode 100644
index 0000000000..5dfdd1aa47
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_history_desktop.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+EnableEngines(["history"]);
+
+var phases = { "phase1": "profile1" };
+
+
+// expected history state
+var historyExpected = [
+ { uri: "http://www.example.com/",
+ visits: [
+ { type: 1 },
+ { type: 2 }
+ ]
+ }
+];
+
+// sync and verify history
+Phase("phase1", [
+ [Sync],
+ [History.verify, historyExpected]
+]);
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_integration.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_integration.py
new file mode 100644
index 0000000000..b0221e9b41
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_integration.py
@@ -0,0 +1,26 @@
+def test_sync_account_settings(tps, gradlewbuild):
+ gradlewbuild.test("checkAccountSettings")
+
+
+def test_sync_history_from_desktop(tps, gradlewbuild):
+ tps.run("test_history.js")
+ gradlewbuild.test("checkHistoryFromDesktopTest")
+
+
+"""
+def test_sync_bookmark_from_desktop(tps, gradlewbuild):
+ tps.run('test_bookmark.js')
+ gradlewbuild.test('checkBookmarkFromDesktopTest')
+
+def test_sync_logins_from_desktop(tps, gradlewbuild):
+ tps.run('test_logins.js')
+ gradlewbuild.test('checkLoginsFromDesktopTest')
+
+def test_sync_bookmark_from_device(tps, gradlewbuild):
+ gradlewbuild.test('checkBookmarkFromDeviceTest')
+ tps.run('test_bookmark_desktop.js')
+
+def test_sync_history_from_device(tps, gradlewbuild):
+ gradlewbuild.test('checkHistoryFromDeviceTest')
+ tps.run('test_history_desktop.js')
+"""
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_logins.js b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_logins.js
new file mode 100644
index 0000000000..16eb4bcf34
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/test_logins.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+EnableEngines(["passwords"]);
+
+var phases = { "phase1": "profile1" };
+
+
+// expected tabs state
+var password_list = [{
+ hostname: "https://accounts.google.com",
+ submitURL: "https://accounts.google.com/signin/challenge/sl/password",
+ realm: null,
+ username: "iosmztest",
+ password: "test15mz",
+ usernameField: "Email",
+ passwordField: "Passwd",
+ }];
+
+// sync and verify tabs
+Phase("phase1", [
+ [Sync],
+ [Passwords.add, password_list],
+ [Sync]
+]);
\ No newline at end of file
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/tps.py b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/tps.py
new file mode 100644
index 0000000000..af3204c794
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/syncintegration/tps.py
@@ -0,0 +1,55 @@
+import logging
+import os
+
+from mozrunner import FirefoxRunner
+
+logging.getLogger(__name__).addHandler(logging.NullHandler())
+
+TIMEOUT = 60
+
+
+class TPS(object):
+ logger = logging.getLogger()
+
+ def __init__(self, firefox, firefox_log, tps_log, profile):
+ self.firefox = firefox
+ self.firefox_log = open(firefox_log, "w")
+ self.tps_log = tps_log
+ self.profile = profile
+
+ def _log(self, line):
+ self.firefox_log.write(line + "\n")
+
+ def run(self, test, phase="phase1", ignore_unused_engines=True):
+ self.profile.set_preferences(
+ {
+ "testing.tps.testFile": os.path.abspath(test),
+ "testing.tps.testPhase": phase,
+ "testing.tps.ignoreUnusedEngines": ignore_unused_engines,
+ }
+ )
+ args = ["-marionette"]
+ process_args = {"processOutputLine": [self._log]}
+ self.logger.info("Running: {} {}".format(self.firefox, " ".join(args)))
+ self.logger.info("Using profile at: {}".format(self.profile.profile))
+ runner = FirefoxRunner(
+ binary=self.firefox,
+ cmdargs=args,
+ profile=self.profile,
+ process_args=process_args,
+ )
+ runner.start(timeout=TIMEOUT)
+ runner.wait(timeout=TIMEOUT)
+ self.firefox_log.close()
+
+ with open(self.tps_log) as f:
+ for line in f.readlines():
+ if "CROSSWEAVE ERROR: " in line:
+ raise TPSError(line.partition("CROSSWEAVE ERROR: ")[-1])
+
+ with open(self.tps_log) as f:
+ assert "test phase {}: PASS".format(phase) in f.read()
+
+
+class TPSError(Exception):
+ pass
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/AddressAutofillTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/AddressAutofillTest.kt
new file mode 100644
index 0000000000..61a3c0b7fc
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/AddressAutofillTest.kt
@@ -0,0 +1,420 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.autofillScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class AddressAutofillTest : TestSetup() {
+ object FirstAddressAutofillDetails {
+ var navigateToAutofillSettings = true
+ var isAddressAutofillEnabled = true
+ var userHasSavedAddress = false
+ var name = "Mozilla Fenix Firefox"
+ var streetAddress = "Harrison Street"
+ var city = "San Francisco"
+ var state = "Alaska"
+ var zipCode = "94105"
+ var country = "United States"
+ var phoneNumber = "555-5555"
+ var emailAddress = "foo@bar.com"
+ }
+
+ object SecondAddressAutofillDetails {
+ var navigateToAutofillSettings = false
+ var name = "Android Test Name"
+ var streetAddress = "Fort Street"
+ var city = "San Jose"
+ var state = "Arizona"
+ var zipCode = "95141"
+ var country = "United States"
+ var phoneNumber = "777-7777"
+ var emailAddress = "fuu@bar.org"
+ }
+
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836845
+ @SmokeTest
+ @Test
+ fun verifyAddressAutofillTest() {
+ val addressFormPage =
+ TestAssetHelper.getAddressFormAsset(mockWebServer)
+
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ }.goBack {
+ }.goBack {
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(addressFormPage.url) {
+ clickPageObject(itemWithResId("streetAddress"))
+ clickSelectAddressButton()
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/address_name",
+ "Harrison Street",
+ ),
+ )
+ verifyAutofilledAddress("Harrison Street")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836856
+ @SmokeTest
+ @Test
+ fun deleteSavedAddressTest() {
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ clickManageAddressesButton()
+ clickSavedAddress("Mozilla")
+ clickDeleteAddressButton()
+ clickCancelDeleteAddressButton()
+ clickDeleteAddressButton()
+ clickConfirmDeleteAddressButton()
+ verifyAddAddressButton()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836840
+ @Test
+ fun verifyAddAddressViewTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ clickAddAddressButton()
+ verifyAddAddressView()
+ }.goBackToAutofillSettings {
+ verifyAutofillToolbarTitle()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836841
+ @Test
+ fun verifyEditAddressViewTest() {
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ clickManageAddressesButton()
+ clickSavedAddress("Mozilla")
+ verifyEditAddressView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836839
+ @Test
+ fun verifyAddressAutofillToggleTest() {
+ val addressFormPage =
+ TestAssetHelper.getAddressFormAsset(mockWebServer)
+
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(addressFormPage.url) {
+ clickPageObject(itemWithResId("streetAddress"))
+ verifySelectAddressButtonExists(true)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ clickSaveAndAutofillAddressesOption()
+ verifyAddressAutofillSection(false, true)
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(addressFormPage.url) {
+ clickPageObject(itemWithResId("streetAddress"))
+ verifySelectAddressButtonExists(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836847
+ @Test
+ fun verifyManageAddressesPromptOptionTest() {
+ val addressFormPage =
+ TestAssetHelper.getAddressFormAsset(mockWebServer)
+
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(addressFormPage.url) {
+ clickPageObject(itemWithResId("streetAddress"))
+ clickSelectAddressButton()
+ }.clickManageAddressButton {
+ verifyAutofillToolbarTitle()
+ }.goBackToBrowser {
+ verifySaveLoginPromptIsNotDisplayed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836849
+ @Test
+ fun verifyMultipleAddressesSelectionTest() {
+ val addressFormPage =
+ TestAssetHelper.getAddressFormAsset(mockWebServer)
+
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ clickManageAddressesButton()
+ clickAddAddressButton()
+ fillAndSaveAddress(
+ navigateToAutofillSettings = SecondAddressAutofillDetails.navigateToAutofillSettings,
+ name = SecondAddressAutofillDetails.name,
+ streetAddress = SecondAddressAutofillDetails.streetAddress,
+ city = SecondAddressAutofillDetails.city,
+ state = SecondAddressAutofillDetails.state,
+ zipCode = SecondAddressAutofillDetails.zipCode,
+ country = SecondAddressAutofillDetails.country,
+ phoneNumber = SecondAddressAutofillDetails.phoneNumber,
+ emailAddress = SecondAddressAutofillDetails.emailAddress,
+ )
+ verifyManageAddressesToolbarTitle()
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(addressFormPage.url) {
+ clickPageObject(itemWithResId("streetAddress"))
+ clickSelectAddressButton()
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/address_name",
+ "Harrison Street",
+ ),
+ )
+ verifyAutofilledAddress("Harrison Street")
+ clearAddressForm()
+ clickPageObject(itemWithResId("streetAddress"))
+ clickSelectAddressButton()
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/address_name",
+ "Fort Street",
+ ),
+ )
+ verifyAutofilledAddress("Fort Street")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836850
+ @Test
+ fun verifySavedAddressCanBeEditedTest() {
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ clickManageAddressesButton()
+ clickSavedAddress("Mozilla")
+ fillAndSaveAddress(
+ navigateToAutofillSettings = SecondAddressAutofillDetails.navigateToAutofillSettings,
+ name = SecondAddressAutofillDetails.name,
+ streetAddress = SecondAddressAutofillDetails.streetAddress,
+ city = SecondAddressAutofillDetails.city,
+ state = SecondAddressAutofillDetails.state,
+ zipCode = SecondAddressAutofillDetails.zipCode,
+ country = SecondAddressAutofillDetails.country,
+ phoneNumber = SecondAddressAutofillDetails.phoneNumber,
+ emailAddress = SecondAddressAutofillDetails.emailAddress,
+ )
+ verifyManageAddressesToolbarTitle()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836848
+ @Test
+ fun verifyStateFieldUpdatesInAccordanceWithCountryFieldTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyAddressAutofillSection(true, false)
+ clickAddAddressButton()
+ verifyCountryOption("United States")
+ verifyStateOption("Alabama")
+ verifyCountryOptions("Canada", "United States")
+ clickCountryOption("Canada")
+ verifyStateOption("Alberta")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836858
+ @Test
+ fun verifyFormFieldCanBeFilledManuallyTest() {
+ val addressFormPage =
+ TestAssetHelper.getAddressFormAsset(mockWebServer)
+
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(addressFormPage.url) {
+ clickPageObject(itemWithResId("streetAddress"))
+ clickSelectAddressButton()
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/address_name",
+ "Harrison Street",
+ ),
+ )
+ verifyAutofilledAddress("Harrison Street")
+ setTextForApartmentTextBox("Ap. 07")
+ verifyManuallyFilledAddress("Ap. 07")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836838
+ @Test
+ fun verifyAutofillAddressSectionTest() {
+ autofillScreen {
+ fillAndSaveAddress(
+ navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings,
+ isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled,
+ userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress,
+ name = FirstAddressAutofillDetails.name,
+ streetAddress = FirstAddressAutofillDetails.streetAddress,
+ city = FirstAddressAutofillDetails.city,
+ state = FirstAddressAutofillDetails.state,
+ zipCode = FirstAddressAutofillDetails.zipCode,
+ country = FirstAddressAutofillDetails.country,
+ phoneNumber = FirstAddressAutofillDetails.phoneNumber,
+ emailAddress = FirstAddressAutofillDetails.emailAddress,
+ )
+ verifyAddressAutofillSection(true, true)
+ clickManageAddressesButton()
+ verifyManageAddressesSection(
+ "Mozilla",
+ "Fenix",
+ "Firefox",
+ "Harrison Street",
+ "San Francisco",
+ "Alaska",
+ "94105",
+ "US",
+ "555-5555",
+ "foo@bar.com",
+ )
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt
new file mode 100644
index 0000000000..96ca6ad724
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt
@@ -0,0 +1,775 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createBookmarkItem
+import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.bookmarksMenu
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic functionality of bookmarks
+ */
+class BookmarksTest : TestSetup() {
+ private val bookmarksFolderName = "New Folder"
+ private val testBookmark = object {
+ var title: String = "Bookmark title"
+ var url: String = "https://www.example.com"
+ }
+
+ @get:Rule(order = 0)
+ val activityTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(),
+ ) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522919
+ @Test
+ fun verifyEmptyBookmarksMenuTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1),
+ ) {
+ verifyBookmarksMenuView()
+ verifyAddFolderButton()
+ verifyCloseButton()
+ verifyBookmarkTitle("Desktop Bookmarks")
+ selectFolder("Desktop Bookmarks")
+ verifyFolderTitle("Bookmarks Menu")
+ verifyFolderTitle("Bookmarks Toolbar")
+ verifyFolderTitle("Other Bookmarks")
+ verifySyncSignInButton()
+ }
+ }.clickSingInToSyncButton {
+ verifyTurnOnSyncToolbarTitle()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301370
+ @Test
+ fun verifyAddBookmarkButtonTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.bookmarkPage {
+ verifySnackBarText("Bookmark saved!")
+ }.openThreeDotMenu {
+ verifyEditBookmarkButton()
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ verifyBookmarksMenuView()
+ verifyBookmarkTitle(defaultWebPage.title)
+ verifyBookmarkedURL(defaultWebPage.url.toString())
+ verifyBookmarkFavicon(defaultWebPage.url)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522920
+ @Test
+ fun cancelCreateBookmarkFolderTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ clickAddFolderButton()
+ addNewFolderName(bookmarksFolderName)
+ navigateUp()
+ verifyKeyboardHidden(isExpectedToBeVisible = false)
+ verifyBookmarkFolderIsNotCreated(bookmarksFolderName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299619
+ @Test
+ fun cancelingChangesInEditModeAreNotSavedTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.bookmarkPage {
+ clickSnackbarButton("EDIT")
+ }
+ bookmarksMenu {
+ verifyEditBookmarksView()
+ changeBookmarkTitle(testBookmark.title)
+ changeBookmarkUrl(testBookmark.url)
+ }.closeEditBookmarkSection {
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ verifyBookmarkTitle(defaultWebPage.title)
+ verifyBookmarkedURL(defaultWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325633
+ @SmokeTest
+ @Test
+ fun editBookmarksNameAndUrlTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.editBookmarkPage {
+ verifyEditBookmarksView()
+ changeBookmarkTitle(testBookmark.title)
+ changeBookmarkUrl(testBookmark.url)
+ saveEditBookmark()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ verifyBookmarkTitle(testBookmark.title)
+ verifyBookmarkedURL(testBookmark.url)
+ }.openBookmarkWithTitle(testBookmark.title) {
+ verifyUrl("example.com")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/341696
+ @Test
+ fun copyBookmarkURLTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickCopy {
+ verifySnackBarText(expectedText = "URL copied")
+ navigateUp()
+ }
+
+ navigationToolbar {
+ }.clickUrlbar {
+ clickClearButton()
+ longClickToolbar()
+ clickPasteText()
+ verifyTypedToolbarText(defaultWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325634
+ @Test
+ fun shareBookmarkTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickShare {
+ verifyShareOverlay()
+ verifyShareBookmarkFavicon()
+ verifyShareBookmarkTitle()
+ verifyShareBookmarkUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325636
+ @Test
+ fun openBookmarkInNewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickOpenInNewTab {
+ verifyTabTrayIsOpened()
+ verifyNormalModeSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1919261
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
+ @Test
+ fun verifyOpenAllInNewTabsOptionTest() {
+ val webPages = listOf(
+ TestAssetHelper.getGenericAsset(mockWebServer, 1),
+ TestAssetHelper.getGenericAsset(mockWebServer, 2),
+ TestAssetHelper.getGenericAsset(mockWebServer, 3),
+ TestAssetHelper.getGenericAsset(mockWebServer, 4),
+ )
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ createFolder("root")
+ createFolder("sub", "root")
+ createFolder("empty", "root")
+ }.closeMenu {
+ }
+
+ browserScreen {
+ createBookmark(webPages[0].url)
+ createBookmark(webPages[1].url, "root")
+ createBookmark(webPages[2].url, "root")
+ createBookmark(webPages[3].url, "sub")
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.openThreeDotMenu("root") {
+ }.clickOpenAllInTabs {
+ verifyTabTrayIsOpened()
+ verifyNormalModeSelected()
+
+ verifyExistingOpenTabs("Test_Page_2", "Test_Page_3", "Test_Page_4")
+
+ // Bookmark that is not under the root folder should not be opened
+ verifyNoExistingOpenTabs("Test_Page_1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1919262
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
+ @Test
+ fun verifyOpenAllInPrivateTabsTest() {
+ val webPages = listOf(
+ TestAssetHelper.getGenericAsset(mockWebServer, 1),
+ TestAssetHelper.getGenericAsset(mockWebServer, 2),
+ )
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ createFolder("root")
+ createFolder("sub", "root")
+ createFolder("empty", "root")
+ }.closeMenu {
+ }
+
+ browserScreen {
+ createBookmark(webPages[0].url, "root")
+ createBookmark(webPages[1].url, "sub")
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.openThreeDotMenu("root") {
+ }.clickOpenAllInPrivateTabs {
+ verifyTabTrayIsOpened()
+ verifyPrivateModeSelected()
+
+ verifyExistingOpenTabs("Test_Page_1", "Test_Page_2")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325637
+ @Test
+ fun openBookmarkInPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickOpenInPrivateTab {
+ verifyTabTrayIsOpened()
+ verifyPrivateModeSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325635
+ @Test
+ fun deleteBookmarkTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickDelete {
+ verifyUndoDeleteSnackBarButton()
+ clickSnackbarButton("UNDO")
+ verifySnackBarHidden()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ verifyBookmarkedURL(defaultWebPage.url.toString())
+ }
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300275
+ @Test
+ fun bookmarksMultiSelectionToolbarItemsTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ verifyMultiSelectionCheckmark(defaultWebPage.url)
+ verifyMultiSelectionCounter()
+ verifyShareBookmarksButton()
+ verifyCloseToolbarButton()
+ }.closeToolbarReturnToBookmarks {
+ verifyBookmarksMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300276
+ @SmokeTest
+ @Test
+ fun openMultipleSelectedBookmarksInANewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenNewTab {
+ verifyNormalModeSelected()
+ verifyExistingTabList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300277
+ @Test
+ fun openMultipleSelectedBookmarksInPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenPrivateTab {
+ verifyPrivateModeSelected()
+ verifyExistingTabList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325644
+ @SmokeTest
+ @Test
+ fun deleteMultipleSelectedBookmarksTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ browserScreen {
+ createBookmark(firstWebPage.url)
+ createBookmark(secondWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ longTapSelectItem(secondWebPage.url)
+ }
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+
+ multipleSelectionToolbar {
+ clickMultiSelectionDelete()
+ }
+
+ bookmarksMenu {
+ verifySnackBarText(expectedText = "Bookmarks deleted")
+ clickSnackbarButton("UNDO")
+ verifyBookmarkedURL(firstWebPage.url.toString())
+ verifyBookmarkedURL(secondWebPage.url.toString())
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ longTapSelectItem(secondWebPage.url)
+ }
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+
+ multipleSelectionToolbar {
+ clickMultiSelectionDelete()
+ }
+
+ bookmarksMenu {
+ verifySnackBarText(expectedText = "Bookmarks deleted")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301355
+ @Test
+ fun shareMultipleSelectedBookmarksTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ clickShareBookmarksButton()
+ verifyShareOverlay()
+ verifyShareTabFavicon()
+ verifyShareTabTitle()
+ verifyShareTabUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325639
+ @Test
+ fun createBookmarkFolderTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickEdit {
+ clickParentFolderSelector()
+ clickAddNewFolderButtonFromSelectFolderView()
+ addNewFolderName(bookmarksFolderName)
+ saveNewFolder()
+ navigateUp()
+ saveEditBookmark()
+ selectFolder(bookmarksFolderName)
+ verifyBookmarkedURL(defaultWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325645
+ @Test
+ fun navigateBookmarksFoldersTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ createFolder("1")
+ getInstrumentation().waitForIdleSync()
+ waitForBookmarksFolderContentToExist("Bookmarks", "1")
+ selectFolder("1")
+ verifyCurrentFolderTitle("1")
+ createFolder("2")
+ getInstrumentation().waitForIdleSync()
+ waitForBookmarksFolderContentToExist("1", "2")
+ selectFolder("2")
+ verifyCurrentFolderTitle("2")
+ navigateUp()
+ waitForBookmarksFolderContentToExist("1", "2")
+ verifyCurrentFolderTitle("1")
+ mDevice.pressBack()
+ verifyBookmarksMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/374855
+ @Test
+ fun cantSelectDefaultFoldersTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)),
+ ) {
+ longTapDesktopFolder("Desktop Bookmarks")
+ verifySnackBarText(expectedText = "Can’t edit default folders")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299703
+ @Test
+ fun deleteBookmarkInEditModeTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickEdit {
+ clickDeleteInEditModeButton()
+ cancelDeletion()
+ clickDeleteInEditModeButton()
+ confirmDeletion()
+ verifySnackBarText(expectedText = "Deleted")
+ verifyBookmarkIsDeleted("Test_Page_1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715710
+ @Test
+ fun verifySearchBookmarksViewTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ createBookmarkItem(defaultWebPage.url.toString(), defaultWebPage.title, 1u)
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ verifySearchView()
+ verifySearchToolbar(true)
+ verifySearchSelectorButton()
+ verifySearchEngineIcon("Bookmarks")
+ verifySearchBarPlaceholder("Search bookmarks")
+ verifySearchBarPosition(true)
+ tapOutsideToDismissSearchBar()
+ verifySearchToolbar(false)
+ }
+
+ runBlocking {
+ // Switching to top toolbar position
+ appContext.settings().shouldUseBottomToolbar = false
+ restartApp(activityTestRule.activityRule)
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ verifySearchToolbar(true)
+ verifySearchEngineIcon("Bookmarks")
+ verifySearchBarPosition(false)
+ pressBack()
+ verifySearchToolbar(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715712
+ @Test
+ fun verifySearchForBookmarkedItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
+
+ createBookmarkItem(firstWebPage.url.toString(), firstWebPage.title, 1u)
+ createBookmarkItem(secondWebPage.url.toString(), secondWebPage.title, 2u)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch(firstWebPage.title)
+ verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString(), searchTerm = firstWebPage.title)
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ }.dismissSearchBar {}
+ bookmarksMenu {
+ }.clickSearchButton {
+ // Search for invalid term
+ typeSearch("Android")
+ verifySuggestionsAreNotDisplayed(
+ activityTestRule,
+ firstWebPage.url.toString(),
+ secondWebPage.url.toString(),
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715711
+ @Test
+ fun verifyVoiceSearchInBookmarksTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ verifySearchToolbar(true)
+ verifySearchEngineIcon("Bookmarks")
+ startVoiceSearch()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715714
+ @Test
+ fun verifyDeletedBookmarksAreNotDisplayedAsSearchResultsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ browserScreen {
+ createBookmark(firstWebPage.url)
+ createBookmark(secondWebPage.url)
+ createBookmark(thirdWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.openThreeDotMenu(firstWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(firstWebPage.title)
+ }.openThreeDotMenu(secondWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(secondWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, firstWebPage.url.toString())
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ verifySearchEngineSuggestionResults(activityTestRule, thirdWebPage.url.toString(), searchTerm = "generic")
+ pressBack()
+ }
+ bookmarksMenu {
+ }.openThreeDotMenu(thirdWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(thirdWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, thirdWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325642
+ @SmokeTest
+ @Test
+ fun deleteBookmarkFoldersTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(website.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ verifyBookmarkTitle("Test_Page_1")
+ createFolder("My Folder")
+ verifyFolderTitle("My Folder")
+ }.openThreeDotMenu("Test_Page_1") {
+ }.clickEdit {
+ clickParentFolderSelector()
+ selectFolder("My Folder")
+ navigateUp()
+ saveEditBookmark()
+ createFolder("My Folder 2")
+ verifyFolderTitle("My Folder 2")
+ }.openThreeDotMenu("My Folder 2") {
+ }.clickEdit {
+ clickParentFolderSelector()
+ selectFolder("My Folder")
+ navigateUp()
+ saveEditBookmark()
+ }.openThreeDotMenu("My Folder") {
+ }.clickDelete {
+ cancelFolderDeletion()
+ verifyFolderTitle("My Folder")
+ }.openThreeDotMenu("My Folder") {
+ }.clickDelete {
+ confirmDeletion()
+ verifySnackBarText(expectedText = "Deleted")
+ clickSnackbarButton("UNDO")
+ verifyFolderTitle("My Folder")
+ }.openThreeDotMenu("My Folder") {
+ }.clickDelete {
+ confirmDeletion()
+ verifySnackBarText(expectedText = "Deleted")
+ verifyBookmarkIsDeleted("My Folder")
+ verifyBookmarkIsDeleted("My Folder 2")
+ verifyBookmarkIsDeleted("Test_Page_1")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BrowsingErrorPagesTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BrowsingErrorPagesTest.kt
new file mode 100644
index 0000000000..608c0e9191
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/BrowsingErrorPagesTest.kt
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests that verify errors encountered while browsing websites: unsafe pages, connection errors, etc
+ */
+class BrowsingErrorPagesTest : TestSetup() {
+ private val malwareWarning = getStringResource(R.string.mozac_browser_errorpages_safe_browsing_malware_uri_title)
+ private val phishingWarning = getStringResource(R.string.mozac_browser_errorpages_safe_phishing_uri_title)
+ private val unwantedSoftwareWarning =
+ getStringResource(R.string.mozac_browser_errorpages_safe_browsing_unwanted_uri_title)
+ private val harmfulSiteWarning = getStringResource(R.string.mozac_browser_errorpages_safe_harmful_uri_title)
+
+ @get: Rule
+ val mActivityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326774
+ @SmokeTest
+ @Test
+ fun verifyMalwareWebsiteWarningMessageTest() {
+ val malwareURl = "http://itisatrap.org/firefox/its-an-attack.html"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(malwareURl.toUri()) {
+ verifyPageContent(malwareWarning)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326773
+ @SmokeTest
+ @Test
+ fun verifyPhishingWebsiteWarningMessageTest() {
+ val phishingURl = "http://itisatrap.org/firefox/its-a-trap.html"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(phishingURl.toUri()) {
+ verifyPageContent(phishingWarning)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326772
+ @SmokeTest
+ @Test
+ fun verifyUnwantedSoftwareWebsiteWarningMessageTest() {
+ val unwantedURl = "http://itisatrap.org/firefox/unwanted.html"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(unwantedURl.toUri()) {
+ verifyPageContent(unwantedSoftwareWarning)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/329877
+ @SmokeTest
+ @Test
+ fun verifyHarmfulWebsiteWarningMessageTest() {
+ val harmfulURl = "https://itisatrap.org/firefox/harmful.html"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(harmfulURl.toUri()) {
+ verifyPageContent(harmfulSiteWarning)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/329882
+ // Failing with network interruption, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1833874
+ // This tests the server ERROR_CONNECTION_REFUSED
+ @Test
+ fun verifyConnectionInterruptedErrorMessageTest() {
+ val testUrl = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testUrl.url) {
+ waitForPageToLoad()
+ verifyPageContent(testUrl.content)
+ // Disconnecting the server
+ mockWebServer.shutdown()
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ verifyConnectionErrorMessage()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/329881
+ @Test
+ fun verifyAddressNotFoundErrorMessageTest() {
+ val url = "ww.example.com"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(url.toUri()) {
+ waitForPageToLoad()
+ verifyAddressNotFoundErrorMessage()
+ clickPageObject(itemWithResId("errorTryAgain"))
+ verifyAddressNotFoundErrorMessage()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2140588
+ @Test
+ fun verifyNoInternetConnectionErrorMessageTest() {
+ val url = "www.example.com"
+
+ setNetworkEnabled(false)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(url.toUri()) {
+ verifyNoInternetConnectionErrorMessage()
+ }
+
+ setNetworkEnabled(true)
+
+ browserScreen {
+ clickPageObject(itemWithResId("errorTryAgain"))
+ waitForPageToLoad()
+ verifyPageContent("Example Domain")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt
new file mode 100644
index 0000000000..3868b622db
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt
@@ -0,0 +1,518 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.collectionRobot
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.tabDrawer
+
+/**
+ * Tests for verifying basic functionality of tab collections
+ *
+ */
+
+class CollectionTest : TestSetup() {
+ private val firstCollectionName = "testcollection_1"
+ private val secondCollectionName = "testcollection_2"
+ private val collectionName = "First Collection"
+
+ @get:Rule
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule(
+ isHomeOnboardingDialogEnabled = false,
+ isJumpBackInCFREnabled = false,
+ isRecentTabsFeatureEnabled = false,
+ isRecentlyVisitedFeatureEnabled = false,
+ isPocketEnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ isTCPCFREnabled = false,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/353823
+ @SmokeTest
+ @Test
+ fun createFirstCollectionUsingHomeScreenButtonTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ mDevice.waitForIdle()
+ }.goToHomescreen {
+ }.clickSaveTabsToCollectionButton {
+ longClickTab(firstWebPage.title)
+ selectTab(secondWebPage.title, numOfTabs = 2)
+ }.clickSaveCollection {
+ typeCollectionNameAndSave(collectionName)
+ }
+
+ tabDrawer {
+ verifySnackBarText("Collection saved!")
+ clickSnackbarButton("VIEW")
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2283299
+ @Test
+ fun createFirstCollectionFromMainMenuTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.openSaveToCollection {
+ verifyCollectionNameTextField()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343422
+ @SmokeTest
+ @Test
+ fun verifyExpandedCollectionItemsTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+ val webPageUrl = webPage.url.host.toString()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openTabDrawer {
+ createCollection(webPage.title, collectionName = collectionName)
+ clickSnackbarButton("VIEW")
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title)
+ verifyCollectionTabUrl(true, webPageUrl)
+ verifyShareCollectionButtonIsVisible(true)
+ verifyCollectionMenuIsVisible(true, composeTestRule)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
+ }.collapseCollection(collectionName) {}
+
+ collectionRobot {
+ verifyTabSavedInCollection(webPage.title, false)
+ verifyShareCollectionButtonIsVisible(false)
+ verifyCollectionMenuIsVisible(false, composeTestRule)
+ verifyCollectionTabUrl(false, webPageUrl)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title)
+ verifyCollectionTabUrl(true, webPageUrl)
+ verifyShareCollectionButtonIsVisible(true)
+ verifyCollectionMenuIsVisible(true, composeTestRule)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
+ }.collapseCollection(collectionName) {}
+
+ collectionRobot {
+ verifyTabSavedInCollection(webPage.title, false)
+ verifyShareCollectionButtonIsVisible(false)
+ verifyCollectionMenuIsVisible(false, composeTestRule)
+ verifyCollectionTabUrl(false, webPageUrl)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343425
+ @SmokeTest
+ @Test
+ fun openAllTabsFromACollectionTest() {
+ val firstTestPage = getGenericAsset(mockWebServer, 1)
+ val secondTestPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstTestPage.url) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondTestPage.url.toString()) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ createCollection(
+ firstTestPage.title,
+ secondTestPage.title,
+ collectionName = collectionName,
+ )
+ closeTab()
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectOpenTabs(composeTestRule)
+ }
+ tabDrawer {
+ verifyExistingOpenTabs(firstTestPage.title, secondTestPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343426
+ @SmokeTest
+ @Test
+ fun shareAllTabsFromACollectionTest() {
+ val firstWebsite = getGenericAsset(mockWebServer, 1)
+ val secondWebsite = getGenericAsset(mockWebServer, 2)
+ val sharingApp = "Gmail"
+ val urlString = "${secondWebsite.url}\n\n${firstWebsite.url}"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebsite.url) {
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebsite.url.toString()) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ createCollection(firstWebsite.title, secondWebsite.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ }.clickShareCollectionButton {
+ verifyShareTabsOverlay(firstWebsite.title, secondWebsite.title)
+ verifySharingWithSelectedApp(sharingApp, urlString, collectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343428
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ @SmokeTest
+ @Test
+ fun deleteCollectionTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openTabDrawer {
+ createCollection(webPage.title, collectionName = collectionName)
+ clickSnackbarButton("VIEW")
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectDeleteCollection(composeTestRule)
+ }
+
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName, true)
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectDeleteCollection(composeTestRule)
+ }
+
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ verifyNoCollectionsText()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2319453
+ // open a webpage, and add currently opened tab to existing collection
+ @Test
+ fun saveTabToExistingCollectionFromMainMenuTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openTabDrawer {
+ createCollection(firstWebPage.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {}
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ }.openThreeDotMenu {
+ }.openSaveToCollection {
+ }.selectExistingCollection(collectionName) {
+ verifySnackBarText("Tab saved!")
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(firstWebPage.title)
+ verifyTabSavedInCollection(secondWebPage.title)
+ }
+ }
+
+ // Testrail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343423
+ @Test
+ fun saveTabToExistingCollectionUsingTheAddTabButtonTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openTabDrawer {
+ createCollection(firstWebPage.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ closeTab()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectAddTabToCollection(composeTestRule)
+ verifyTabsSelectedCounterText(1)
+ saveTabsSelectedForCollection()
+ verifySnackBarText("Tab saved!")
+ verifyTabSavedInCollection(secondWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343424
+ @Test
+ fun renameCollectionTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openTabDrawer {
+ createCollection(webPage.title, collectionName = firstCollectionName)
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(firstCollectionName)
+ }.expandCollection(firstCollectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectRenameCollection(composeTestRule)
+ }.typeCollectionNameAndSave(secondCollectionName) {}
+
+ homeScreen {
+ verifyCollectionIsDisplayed(secondCollectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991248
+ @Test
+ fun createCollectionUsingSelectTabsButtonTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ }.openTabDrawer {
+ createCollection(
+ tabTitles = arrayOf(firstWebPage.title, secondWebPage.title),
+ collectionName = firstCollectionName,
+ )
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(firstCollectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2319455
+ @Test
+ fun removeTabFromCollectionUsingTheCloseButtonTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openTabDrawer {
+ createCollection(webPage.title, collectionName = collectionName)
+ closeTab()
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title, true)
+ removeTabFromCollection(webPage.title)
+ }
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title, true)
+ removeTabFromCollection(webPage.title)
+ verifyTabSavedInCollection(webPage.title, false)
+ }
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343427
+ @Test
+ fun removeTabFromCollectionUsingSwipeLeftActionTest() {
+ val testPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.url) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ createCollection(
+ testPage.title,
+ collectionName = collectionName,
+ )
+ closeTab()
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ swipeTabLeft(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(testPage.title, true)
+ swipeTabLeft(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991278
+ @Test
+ fun removeTabFromCollectionUsingSwipeRightActionTest() {
+ val testPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.url) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ createCollection(
+ testPage.title,
+ collectionName = collectionName,
+ )
+ closeTab()
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ swipeTabRight(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(testPage.title, true)
+ swipeTabRight(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991276
+ @Test
+ fun createCollectionByLongPressingOpenTabsTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ verifyExistingOpenTabs(firstWebPage.title, secondWebPage.title)
+ longClickTab(firstWebPage.title)
+ verifyTabsMultiSelectionCounter(1)
+ selectTab(secondWebPage.title, numOfTabs = 2)
+ }.clickSaveCollection {
+ typeCollectionNameAndSave(collectionName)
+ verifySnackBarText("Collection saved!")
+ }
+
+ tabDrawer {
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(firstWebPage.title)
+ verifyTabSavedInCollection(secondWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/344897
+ @Test
+ fun navigateBackInCollectionFlowTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openTabDrawer {
+ createCollection(webPage.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {
+ }.openThreeDotMenu {
+ }.openSaveToCollection {
+ verifySelectCollectionScreen()
+ goBackInCollectionFlow()
+ }
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSaveToCollection {
+ verifySelectCollectionScreen()
+ clickAddNewCollection()
+ verifyCollectionNameTextField()
+ goBackInCollectionFlow()
+ verifySelectCollectionScreen()
+ goBackInCollectionFlow()
+ }
+ // verify the browser layout is visible
+ browserScreen {
+ verifyMenuButton()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeBookmarksTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeBookmarksTest.kt
new file mode 100644
index 0000000000..398533830a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeBookmarksTest.kt
@@ -0,0 +1,753 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.bookmarksMenu
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic functionality of bookmarks
+ */
+class ComposeBookmarksTest : TestSetup() {
+ private val bookmarksFolderName = "New Folder"
+ private val testBookmark = object {
+ var title: String = "Bookmark title"
+ var url: String = "https://www.example.com"
+ }
+
+ @get:Rule(order = 0)
+ val activityTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522919
+ @Test
+ fun verifyEmptyBookmarksMenuTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1),
+ ) {
+ verifyBookmarksMenuView()
+ verifyAddFolderButton()
+ verifyCloseButton()
+ verifyBookmarkTitle("Desktop Bookmarks")
+ selectFolder("Desktop Bookmarks")
+ verifyFolderTitle("Bookmarks Menu")
+ verifyFolderTitle("Bookmarks Toolbar")
+ verifyFolderTitle("Other Bookmarks")
+ verifySyncSignInButton()
+ }
+ }.clickSingInToSyncButton {
+ verifyTurnOnSyncToolbarTitle()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522920
+ @Test
+ fun cancelCreateBookmarkFolderTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ clickAddFolderButton()
+ addNewFolderName(bookmarksFolderName)
+ navigateUp()
+ verifyKeyboardHidden(isExpectedToBeVisible = false)
+ verifyBookmarkFolderIsNotCreated(bookmarksFolderName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299619
+ @Test
+ fun cancelingChangesInEditModeAreNotSavedTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.bookmarkPage {
+ clickSnackbarButton("EDIT")
+ }
+ bookmarksMenu {
+ verifyEditBookmarksView()
+ changeBookmarkTitle(testBookmark.title)
+ changeBookmarkUrl(testBookmark.url)
+ }.closeEditBookmarkSection {
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ verifyBookmarkTitle(defaultWebPage.title)
+ verifyBookmarkedURL(defaultWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325633
+ @SmokeTest
+ @Test
+ fun editBookmarksNameAndUrlTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.editBookmarkPage {
+ verifyEditBookmarksView()
+ changeBookmarkTitle(testBookmark.title)
+ changeBookmarkUrl(testBookmark.url)
+ saveEditBookmark()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ verifyBookmarkTitle(testBookmark.title)
+ verifyBookmarkedURL(testBookmark.url)
+ }.openBookmarkWithTitle(testBookmark.title) {
+ verifyUrl("example.com")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/341696
+ @Test
+ fun copyBookmarkURLTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickCopy {
+ verifySnackBarText(expectedText = "URL copied")
+ navigateUp()
+ }
+
+ navigationToolbar {
+ }.clickUrlbar {
+ clickClearButton()
+ longClickToolbar()
+ clickPasteText()
+ verifyTypedToolbarText(defaultWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325634
+ @Test
+ fun shareBookmarkTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickShare {
+ verifyShareOverlay()
+ verifyShareBookmarkFavicon()
+ verifyShareBookmarkTitle()
+ verifyShareBookmarkUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325636
+ @Test
+ fun openBookmarkInNewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickOpenInNewTab(activityTestRule) {
+ verifyTabTrayIsOpen()
+ verifyNormalBrowsingButtonIsSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1919261
+ @Test
+ fun verifyOpenAllInNewTabsOptionTest() {
+ val webPages = listOf(
+ TestAssetHelper.getGenericAsset(mockWebServer, 1),
+ TestAssetHelper.getGenericAsset(mockWebServer, 2),
+ TestAssetHelper.getGenericAsset(mockWebServer, 3),
+ TestAssetHelper.getGenericAsset(mockWebServer, 4),
+ )
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ createFolder("root")
+ createFolder("sub", "root")
+ createFolder("empty", "root")
+ }.closeMenu {
+ }
+
+ browserScreen {
+ createBookmark(webPages[0].url)
+ createBookmark(webPages[1].url, "root")
+ createBookmark(webPages[2].url, "root")
+ createBookmark(webPages[3].url, "sub")
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTab()
+ }
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.openThreeDotMenu("root") {
+ }.clickOpenAllInTabs(activityTestRule) {
+ verifyTabTrayIsOpen()
+ verifyNormalBrowsingButtonIsSelected()
+
+ verifyExistingOpenTabs("Test_Page_2", "Test_Page_3", "Test_Page_4")
+
+ // Bookmark that is not under the root folder should not be opened
+ verifyNoExistingOpenTabs("Test_Page_1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1919262
+ @Test
+ fun verifyOpenAllInPrivateTabsTest() {
+ val webPages = listOf(
+ TestAssetHelper.getGenericAsset(mockWebServer, 1),
+ TestAssetHelper.getGenericAsset(mockWebServer, 2),
+ )
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ createFolder("root")
+ createFolder("sub", "root")
+ createFolder("empty", "root")
+ }.closeMenu {
+ }
+
+ browserScreen {
+ createBookmark(webPages[0].url, "root")
+ createBookmark(webPages[1].url, "sub")
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTab()
+ }
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.openThreeDotMenu("root") {
+ }.clickOpenAllInPrivateTabs(activityTestRule) {
+ verifyTabTrayIsOpen()
+ verifyPrivateBrowsingButtonIsSelected()
+
+ verifyExistingOpenTabs("Test_Page_1", "Test_Page_2")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325637
+ @Test
+ fun openBookmarkInPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickOpenInPrivateTab(activityTestRule) {
+ verifyTabTrayIsOpen()
+ verifyPrivateBrowsingButtonIsSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325635
+ @Test
+ fun deleteBookmarkTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickDelete {
+ verifyUndoDeleteSnackBarButton()
+ clickSnackbarButton("UNDO")
+ verifySnackBarHidden()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ verifyBookmarkedURL(defaultWebPage.url.toString())
+ }
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300275
+ @Test
+ fun bookmarksMultiSelectionToolbarItemsTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ verifyMultiSelectionCheckmark(defaultWebPage.url)
+ verifyMultiSelectionCounter()
+ verifyShareBookmarksButton()
+ verifyCloseToolbarButton()
+ }.closeToolbarReturnToBookmarks {
+ verifyBookmarksMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300276
+ @SmokeTest
+ @Test
+ fun openMultipleSelectedBookmarksInANewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTab()
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenNewTab(activityTestRule) {
+ verifyTabTrayIsOpen()
+ verifyNormalBrowsingButtonIsSelected()
+ verifyNormalTabsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300277
+ @Test
+ fun openMultipleSelectedBookmarksInPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenPrivateTab(activityTestRule) {
+ verifyPrivateBrowsingButtonIsSelected()
+ verifyPrivateTabsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325644
+ @SmokeTest
+ @Test
+ fun deleteMultipleSelectedBookmarksTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ browserScreen {
+ createBookmark(firstWebPage.url)
+ createBookmark(secondWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ longTapSelectItem(secondWebPage.url)
+ }
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+
+ multipleSelectionToolbar {
+ clickMultiSelectionDelete()
+ }
+
+ bookmarksMenu {
+ verifySnackBarText(expectedText = "Bookmarks deleted")
+ clickSnackbarButton("UNDO")
+ verifyBookmarkedURL(firstWebPage.url.toString())
+ verifyBookmarkedURL(secondWebPage.url.toString())
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ longTapSelectItem(secondWebPage.url)
+ }
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+
+ multipleSelectionToolbar {
+ clickMultiSelectionDelete()
+ }
+
+ bookmarksMenu {
+ verifySnackBarText(expectedText = "Bookmarks deleted")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301355
+ @Test
+ fun shareMultipleSelectedBookmarksTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {
+ longTapSelectItem(defaultWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ clickShareBookmarksButton()
+ verifyShareOverlay()
+ verifyShareTabFavicon()
+ verifyShareTabTitle()
+ verifyShareTabUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325639
+ @Test
+ fun createBookmarkFolderTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickEdit {
+ clickParentFolderSelector()
+ clickAddNewFolderButtonFromSelectFolderView()
+ addNewFolderName(bookmarksFolderName)
+ saveNewFolder()
+ navigateUp()
+ saveEditBookmark()
+ selectFolder(bookmarksFolderName)
+ verifyBookmarkedURL(defaultWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325645
+ @Test
+ fun navigateBookmarksFoldersTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ createFolder("1")
+ getInstrumentation().waitForIdleSync()
+ waitForBookmarksFolderContentToExist("Bookmarks", "1")
+ selectFolder("1")
+ verifyCurrentFolderTitle("1")
+ createFolder("2")
+ getInstrumentation().waitForIdleSync()
+ waitForBookmarksFolderContentToExist("1", "2")
+ selectFolder("2")
+ verifyCurrentFolderTitle("2")
+ navigateUp()
+ waitForBookmarksFolderContentToExist("1", "2")
+ verifyCurrentFolderTitle("1")
+ mDevice.pressBack()
+ verifyBookmarksMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/374855
+ @Test
+ fun cantSelectDefaultFoldersTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)),
+ ) {
+ longTapDesktopFolder("Desktop Bookmarks")
+ verifySnackBarText(expectedText = "Can’t edit default folders")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299703
+ @Test
+ fun deleteBookmarkInEditModeTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
+ ) {}
+ }.openThreeDotMenu(defaultWebPage.title) {
+ }.clickEdit {
+ clickDeleteInEditModeButton()
+ cancelDeletion()
+ clickDeleteInEditModeButton()
+ confirmDeletion()
+ verifySnackBarText(expectedText = "Deleted")
+ verifyBookmarkIsDeleted("Test_Page_1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715710
+ @Test
+ fun verifySearchBookmarksViewTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ verifySearchView()
+ verifySearchToolbar(true)
+ verifySearchSelectorButton()
+ verifySearchEngineIcon("Bookmarks")
+ verifySearchBarPlaceholder("Search bookmarks")
+ verifySearchBarPosition(true)
+ tapOutsideToDismissSearchBar()
+ verifySearchToolbar(false)
+ }
+ bookmarksMenu {
+ }.goBackToBrowserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ clickTopToolbarToggle()
+ }
+
+ exitMenu()
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ verifySearchToolbar(true)
+ verifySearchEngineIcon("Bookmarks")
+ verifySearchBarPosition(false)
+ pressBack()
+ verifySearchToolbar(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715712
+ @Test
+ fun verifySearchForBookmarkedItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ createFolder(bookmarksFolderName)
+ }
+
+ exitMenu()
+
+ browserScreen {
+ createBookmark(firstWebPage.url, bookmarksFolderName)
+ createBookmark(secondWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch(firstWebPage.title)
+ verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString(), searchTerm = firstWebPage.title)
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ // Search for invalid term
+ typeSearch("Android")
+ verifySuggestionsAreNotDisplayed(activityTestRule, firstWebPage.url.toString())
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715711
+ @Test
+ fun verifyVoiceSearchInBookmarksTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(defaultWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.clickSearchButton {
+ verifySearchToolbar(true)
+ verifySearchEngineIcon("Bookmarks")
+ startVoiceSearch()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715714
+ @Test
+ fun verifyDeletedBookmarksAreNotDisplayedAsSearchResultsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ browserScreen {
+ createBookmark(firstWebPage.url)
+ createBookmark(secondWebPage.url)
+ createBookmark(thirdWebPage.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ }.openThreeDotMenu(firstWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(firstWebPage.title)
+ }.openThreeDotMenu(secondWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(secondWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, firstWebPage.url.toString())
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ verifySearchEngineSuggestionResults(activityTestRule, thirdWebPage.url.toString(), searchTerm = "generic")
+ pressBack()
+ }
+ bookmarksMenu {
+ }.openThreeDotMenu(thirdWebPage.title) {
+ }.clickDelete {
+ verifyBookmarkIsDeleted(thirdWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, thirdWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325642
+ // Verifies that deleting a Bookmarks folder also removes the item from inside it.
+ @SmokeTest
+ @Test
+ fun deleteBookmarkFoldersTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ browserScreen {
+ createBookmark(website.url)
+ }.openThreeDotMenu {
+ }.openBookmarks {
+ verifyBookmarkTitle("Test_Page_1")
+ createFolder("My Folder")
+ verifyFolderTitle("My Folder")
+ }.openThreeDotMenu("Test_Page_1") {
+ }.clickEdit {
+ clickParentFolderSelector()
+ selectFolder("My Folder")
+ navigateUp()
+ saveEditBookmark()
+ createFolder("My Folder 2")
+ verifyFolderTitle("My Folder 2")
+ }.openThreeDotMenu("My Folder 2") {
+ }.clickEdit {
+ clickParentFolderSelector()
+ selectFolder("My Folder")
+ navigateUp()
+ saveEditBookmark()
+ }.openThreeDotMenu("My Folder") {
+ }.clickDelete {
+ cancelFolderDeletion()
+ verifyFolderTitle("My Folder")
+ }.openThreeDotMenu("My Folder") {
+ }.clickDelete {
+ confirmDeletion()
+ verifySnackBarText(expectedText = "Deleted")
+ clickSnackbarButton("UNDO")
+ verifyFolderTitle("My Folder")
+ }.openThreeDotMenu("My Folder") {
+ }.clickDelete {
+ confirmDeletion()
+ verifySnackBarText(expectedText = "Deleted")
+ verifyBookmarkIsDeleted("My Folder")
+ verifyBookmarkIsDeleted("My Folder 2")
+ verifyBookmarkIsDeleted("Test_Page_1")
+ navigateUp()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCollectionTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCollectionTest.kt
new file mode 100644
index 0000000000..23c64be289
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCollectionTest.kt
@@ -0,0 +1,502 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.collectionRobot
+import org.mozilla.fenix.ui.robots.composeTabDrawer
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic functionality of tab collections
+ *
+ */
+
+class ComposeCollectionTest : TestSetup() {
+ private val firstCollectionName = "testcollection_1"
+ private val secondCollectionName = "testcollection_2"
+ private val collectionName = "First Collection"
+
+ @get:Rule
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule(
+ isHomeOnboardingDialogEnabled = false,
+ isJumpBackInCFREnabled = false,
+ isRecentTabsFeatureEnabled = false,
+ isRecentlyVisitedFeatureEnabled = false,
+ isPocketEnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ isTCPCFREnabled = false,
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/353823
+ @SmokeTest
+ @Test
+ fun createFirstCollectionUsingHomeScreenButtonTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ mDevice.waitForIdle()
+ }.goToHomescreen {
+ }.clickSaveTabsToCollectionButton(composeTestRule) {
+ longClickTab(firstWebPage.title)
+ selectTab(secondWebPage.title, numberOfSelectedTabs = 2)
+ verifyTabsMultiSelectionCounter(2)
+ }.clickSaveCollection {
+ typeCollectionNameAndSave(collectionName)
+ }
+
+ composeTabDrawer(composeTestRule) {
+ verifySnackBarText("Collection saved!")
+ clickSnackbarButton("VIEW")
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343422
+ @SmokeTest
+ @Test
+ fun verifyExpandedCollectionItemsTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+ val webPageUrl = webPage.url.host.toString()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(webPage.title, collectionName = collectionName)
+ clickSnackbarButton("VIEW")
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title)
+ verifyCollectionTabUrl(true, webPageUrl)
+ verifyShareCollectionButtonIsVisible(true)
+ verifyCollectionMenuIsVisible(true, composeTestRule)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
+ }.collapseCollection(collectionName) {}
+
+ collectionRobot {
+ verifyTabSavedInCollection(webPage.title, false)
+ verifyShareCollectionButtonIsVisible(false)
+ verifyCollectionMenuIsVisible(false, composeTestRule)
+ verifyCollectionTabUrl(false, webPageUrl)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title)
+ verifyCollectionTabUrl(true, webPageUrl)
+ verifyShareCollectionButtonIsVisible(true)
+ verifyCollectionMenuIsVisible(true, composeTestRule)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
+ }.collapseCollection(collectionName) {}
+
+ collectionRobot {
+ verifyTabSavedInCollection(webPage.title, false)
+ verifyShareCollectionButtonIsVisible(false)
+ verifyCollectionMenuIsVisible(false, composeTestRule)
+ verifyCollectionTabUrl(false, webPageUrl)
+ verifyCollectionItemRemoveButtonIsVisible(webPage.title, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343425
+ @SmokeTest
+ @Test
+ fun openAllTabsFromACollectionTest() {
+ val firstTestPage = getGenericAsset(mockWebServer, 1)
+ val secondTestPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstTestPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondTestPage.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(
+ firstTestPage.title,
+ secondTestPage.title,
+ collectionName = collectionName,
+ )
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectOpenTabs(composeTestRule)
+ }
+ composeTabDrawer(composeTestRule) {
+ verifyExistingOpenTabs(firstTestPage.title, secondTestPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343426
+ @SmokeTest
+ @Test
+ fun shareAllTabsFromACollectionTest() {
+ val firstWebsite = getGenericAsset(mockWebServer, 1)
+ val secondWebsite = getGenericAsset(mockWebServer, 2)
+ val sharingApp = "Gmail"
+ val urlString = "${secondWebsite.url}\n\n${firstWebsite.url}"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebsite.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebsite.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(firstWebsite.title, secondWebsite.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ }.clickShareCollectionButton {
+ verifyShareTabsOverlay(firstWebsite.title, secondWebsite.title)
+ verifySharingWithSelectedApp(sharingApp, urlString, collectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343428
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ @SmokeTest
+ @Test
+ fun deleteCollectionTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(webPage.title, collectionName = collectionName)
+ clickSnackbarButton("VIEW")
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectDeleteCollection(composeTestRule)
+ }
+
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName, true)
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectDeleteCollection(composeTestRule)
+ }
+
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ verifyNoCollectionsText()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2319453
+ // open a webpage, and add currently opened tab to existing collection
+ @Test
+ fun saveTabToExistingCollectionFromMainMenuTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(firstWebPage.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {}
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ }.openThreeDotMenu {
+ }.openSaveToCollection {
+ }.selectExistingCollection(collectionName) {
+ verifySnackBarText("Tab saved!")
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(firstWebPage.title)
+ verifyTabSavedInCollection(secondWebPage.title)
+ }
+ }
+
+ // Testrail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343423
+ @Test
+ fun saveTabToExistingCollectionUsingTheAddTabButtonTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(firstWebPage.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ closeTab()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectAddTabToCollection(composeTestRule)
+ verifyTabsSelectedCounterText(1)
+ saveTabsSelectedForCollection()
+ verifySnackBarText("Tab saved!")
+ verifyTabSavedInCollection(secondWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343424
+ @Test
+ fun renameCollectionTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(webPage.title, collectionName = firstCollectionName)
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(firstCollectionName)
+ }.expandCollection(firstCollectionName) {
+ clickCollectionThreeDotButton(composeTestRule)
+ selectRenameCollection(composeTestRule)
+ }.typeCollectionNameAndSave(secondCollectionName) {}
+
+ homeScreen {
+ verifyCollectionIsDisplayed(secondCollectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991248
+ @Test
+ fun createCollectionUsingSelectTabsButtonTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(
+ tabTitles = arrayOf(firstWebPage.title, secondWebPage.title),
+ collectionName = firstCollectionName,
+ )
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ verifyCollectionIsDisplayed(firstCollectionName)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2319455
+ @Test
+ fun removeTabFromCollectionUsingTheCloseButtonTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(webPage.title, collectionName = collectionName)
+ closeTab()
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title, true)
+ removeTabFromCollection(webPage.title)
+ }
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(webPage.title, true)
+ removeTabFromCollection(webPage.title)
+ verifyTabSavedInCollection(webPage.title, false)
+ }
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343427
+ @Test
+ fun removeTabFromCollectionUsingSwipeLeftActionTest() {
+ val testPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(
+ testPage.title,
+ collectionName = collectionName,
+ )
+ closeTab()
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ swipeTabLeft(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(testPage.title, true)
+ swipeTabLeft(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991278
+ @Test
+ fun removeTabFromCollectionUsingSwipeRightActionTest() {
+ val testPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(
+ testPage.title,
+ collectionName = collectionName,
+ )
+ closeTab()
+ }
+
+ homeScreen {
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ swipeTabRight(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ homeScreen {
+ verifySnackBarText("Collection deleted")
+ clickSnackbarButton("UNDO")
+ verifyCollectionIsDisplayed(collectionName)
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(testPage.title, true)
+ swipeTabRight(testPage.title, composeTestRule)
+ verifyTabSavedInCollection(testPage.title, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991276
+ @Test
+ fun createCollectionByLongPressingOpenTabsTest() {
+ val firstWebPage = getGenericAsset(mockWebServer, 1)
+ val secondWebPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyExistingOpenTabs(firstWebPage.title, secondWebPage.title)
+ longClickTab(firstWebPage.title)
+ verifyTabsMultiSelectionCounter(1)
+ selectTab(secondWebPage.title, numberOfSelectedTabs = 2)
+ verifyTabsMultiSelectionCounter(2)
+ }.clickSaveCollection {
+ typeCollectionNameAndSave(collectionName)
+ verifySnackBarText("Collection saved!")
+ }
+
+ composeTabDrawer(composeTestRule) {
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ }.expandCollection(collectionName) {
+ verifyTabSavedInCollection(firstWebPage.title)
+ verifyTabSavedInCollection(secondWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/344897
+ @Test
+ fun navigateBackInCollectionFlowTest() {
+ val webPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ createCollection(webPage.title, collectionName = collectionName)
+ verifySnackBarText("Collection saved!")
+ }.closeTabDrawer {
+ }.openThreeDotMenu {
+ }.openSaveToCollection {
+ verifySelectCollectionScreen()
+ goBackInCollectionFlow()
+ }
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSaveToCollection {
+ verifySelectCollectionScreen()
+ clickAddNewCollection()
+ verifyCollectionNameTextField()
+ goBackInCollectionFlow()
+ verifySelectCollectionScreen()
+ goBackInCollectionFlow()
+ }
+ // verify the browser layout is visible
+ browserScreen {
+ verifyMenuButton()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeContextMenusTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeContextMenusTest.kt
new file mode 100644
index 0000000000..0261c72d89
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeContextMenusTest.kt
@@ -0,0 +1,269 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickContextMenuItem
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.downloadRobot
+import org.mozilla.fenix.ui.robots.longClickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.shareOverlay
+
+/**
+ * Tests for verifying basic functionality of content context menus
+ *
+ * - Verifies long click "Open link in new tab" UI and functionality
+ * - Verifies long click "Open link in new Private tab" UI and functionality
+ * - Verifies long click "Copy Link" UI and functionality
+ * - Verifies long click "Share Link" UI and functionality
+ * - Verifies long click "Open image in new tab" UI and functionality
+ * - Verifies long click "Save Image" UI and functionality
+ * - Verifies long click "Copy image location" UI and functionality
+ * - Verifies long click items of mixed hypertext items
+ *
+ */
+
+class ComposeContextMenusTest : TestSetup() {
+
+ @get:Rule(order = 0)
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule(
+ tabsTrayRewriteEnabled = true,
+ isJumpBackInCFREnabled = false,
+ ),
+ ) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243837
+ @Test
+ fun verifyOpenLinkNewTabContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 1"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Open link in new tab")
+ verifySnackBarText("New tab opened")
+ clickSnackbarButton("SWITCH")
+ verifyUrl(genericURL.url.toString())
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNormalBrowsingButtonIsSelected()
+ verifyExistingOpenTabs("Test_Page_1")
+ verifyExistingOpenTabs("Test_Page_4")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244655
+ @Test
+ fun verifyOpenLinkInNewPrivateTabContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 2"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Open link in private tab")
+ verifySnackBarText("New private tab opened")
+ clickSnackbarButton("SWITCH")
+ verifyUrl(genericURL.url.toString())
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyPrivateBrowsingButtonIsSelected()
+ verifyExistingOpenTabs("Test_Page_2")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243832
+ @Test
+ fun verifyCopyLinkContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 3"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Copy link")
+ verifySnackBarText("Link copied to clipboard")
+ }.openNavigationToolbar {
+ }.visitLinkFromClipboard {
+ verifyUrl(genericURL.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243838
+ @Test
+ fun verifyShareLinkContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 1"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Share link")
+ shareOverlay {
+ verifyShareLinkIntent(genericURL.url)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243833
+ @Test
+ fun verifyOpenImageNewTabContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ clickContextMenuItem("Open image in new tab")
+ verifySnackBarText("New tab opened")
+ clickSnackbarButton("SWITCH")
+ verifyUrl(imageResource.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243834
+ @Test
+ fun verifyCopyImageLocationContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ clickContextMenuItem("Copy image location")
+ verifySnackBarText("Link copied to clipboard")
+ }.openNavigationToolbar {
+ }.visitLinkFromClipboard {
+ verifyUrl(imageResource.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243835
+ @Test
+ fun verifySaveImageContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ clickContextMenuItem("Save image")
+ }
+
+ downloadRobot {
+ verifyDownloadCompleteNotificationPopup()
+ }.clickOpen("image/jpeg") {} // verify open intent is matched with associated data type
+ downloadRobot {
+ verifyPhotosAppOpens()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/352050
+ @Test
+ fun verifyContextMenuLinkVariationsTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 1"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ dismissContentContextMenu()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ dismissContentContextMenu()
+ longClickPageObject(itemWithText("test_no_link_image"))
+ verifyNoLinkImageContextMenuItems(imageResource.url)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2333840
+ @Test
+ fun verifyPDFContextMenuLinkVariationsTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ waitForPageToLoad()
+ longClickPageObject(itemWithText("Wikipedia link"))
+ verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
+ dismissContentContextMenu()
+ // Some options are missing from the linked and non liked images context menus in PDF files
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1012805 for more details
+ longClickPDFImage()
+ verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
+ dismissContentContextMenu()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/832094
+ @Test
+ fun verifyOpenLinkInAppContextMenuOptionTest() {
+ val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ longClickPageObject(itemContainingText("Youtube full link"))
+ verifyContextMenuForLinksToOtherApps("youtube.com")
+ clickContextMenuItem("Open link in external app")
+ assertExternalAppOpens(Constants.PackageName.YOUTUBE_APP)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCrashReportingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCrashReportingTest.kt
new file mode 100644
index 0000000000..e815972900
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCrashReportingTest.kt
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class ComposeCrashReportingTest : TestSetup() {
+ private val tabCrashMessage = getStringResource(R.string.tab_crash_title_2)
+
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityIntentTestRule(
+ isPocketEnabled = false,
+ isJumpBackInCFREnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ isTCPCFREnabled = false,
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/308906
+ @Test
+ fun closeTabFromCrashedTabReporterTest() {
+ homeScreen {
+ }.openNavigationToolbar {
+ }.openTabCrashReporter {
+ }.clickTabCrashedCloseButton {
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyNoOpenTabsInNormalBrowsing()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2336134
+ @Ignore("Test failure caused by: https://github.com/mozilla-mobile/fenix/issues/19964")
+ @Test
+ fun restoreTabFromTabCrashedReporterTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(website.url) {}
+
+ navigationToolbar {
+ }.openTabCrashReporter {
+ clickPageObject(itemWithResId("$packageName:id/restoreTabButton"))
+ verifyPageContent(website.content)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1681928
+ @SmokeTest
+ @Test
+ fun useAppWhileTabIsCrashedTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ waitForPageToLoad()
+ }
+
+ navigationToolbar {
+ }.openTabCrashReporter {
+ verifyPageContent(tabCrashMessage)
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyExistingOpenTabs(firstWebPage.title)
+ verifyExistingOpenTabs("about:crashcontent")
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ }.openThreeDotMenu {
+ verifySettingsButton()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHistoryTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHistoryTest.kt
new file mode 100644
index 0000000000..d3b17f57d9
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHistoryTest.kt
@@ -0,0 +1,421 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import androidx.test.espresso.Espresso.pressBack
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.historyMenu
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic functionality of history
+ *
+ */
+class ComposeHistoryTest : TestSetup() {
+ @get:Rule
+ val activityTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule(
+ tabsTrayRewriteEnabled = true,
+ isJumpBackInCFREnabled = false,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243285
+ @Test
+ fun verifyEmptyHistoryMenuTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ verifyHistoryButton()
+ }.openHistory {
+ verifyHistoryMenuView()
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2302742
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ @SmokeTest
+ @Test
+ fun verifyHistoryMenuWithHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ verifyHistoryMenuView()
+ verifyVisitedTimeTitle()
+ verifyFirstTestPageTitle("Test_Page_1")
+ verifyTestPageUrl(firstWebPage.url)
+ verifyDeleteHistoryItemButton("Test_Page_1")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243288
+ @Test
+ fun deleteHistoryItemTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ clickDeleteHistoryButton(firstWebPage.url.toString())
+ }
+ verifySnackBarText(expectedText = "Deleted")
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1848881
+ @SmokeTest
+ @Test
+ fun deleteAllHistoryTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ clickDeleteAllHistoryButton()
+ }
+ verifyDeleteConfirmationMessage()
+ selectEverythingOption()
+ confirmDeleteAllHistory()
+ verifySnackBarText(expectedText = "Browsing data deleted")
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339690
+ @SmokeTest
+ @Test
+ fun historyMultiSelectionToolbarItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ verifyMultiSelectionCheckmark()
+ verifyMultiSelectionCounter()
+ verifyShareHistoryButton()
+ verifyCloseToolbarButton()
+ }.closeToolbarReturnToHistory {
+ verifyHistoryMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339696
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
+ @Test
+ fun openMultipleSelectedHistoryItemsInANewTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTab()
+ }
+
+ homeScreen { }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenNewTab(activityTestRule) {
+ verifyNormalTabsList()
+ verifyNormalBrowsingButtonIsSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/346098
+ @Test
+ fun openMultipleSelectedHistoryItemsInPrivateTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenPrivateTab(activityTestRule) {
+ verifyPrivateTabsList()
+ verifyPrivateBrowsingButtonIsSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/346099
+ @Test
+ fun deleteMultipleSelectedHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ mDevice.waitForIdle()
+ verifyUrl(secondWebPage.url.toString())
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 2),
+ ) {
+ verifyHistoryItemExists(true, firstWebPage.url.toString())
+ verifyHistoryItemExists(true, secondWebPage.url.toString())
+ longTapSelectItem(firstWebPage.url)
+ longTapSelectItem(secondWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ clickMultiSelectionDelete()
+ }
+
+ historyMenu {
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339701
+ @Test
+ fun shareMultipleSelectedHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ clickShareHistoryButton()
+ verifyShareOverlay()
+ verifyShareTabFavicon()
+ verifyShareTabTitle()
+ verifyShareTabUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715627
+ @Test
+ fun verifySearchHistoryViewTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ verifySearchView()
+ verifySearchToolbar(true)
+ verifySearchSelectorButton()
+ verifySearchEngineIcon("history")
+ verifySearchBarPlaceholder("Search history")
+ verifySearchBarPosition(true)
+ tapOutsideToDismissSearchBar()
+ verifySearchToolbar(false)
+ exitMenu()
+ }
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ clickTopToolbarToggle()
+ }
+
+ exitMenu()
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ verifySearchView()
+ verifySearchToolbar(true)
+ verifySearchBarPosition(false)
+ pressBack()
+ }
+ historyMenu {
+ verifyHistoryMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715631
+ @Test
+ fun verifyVoiceSearchInHistoryTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ verifySearchToolbar(true)
+ verifySearchEngineIcon("history")
+ startVoiceSearch()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715632
+ @Test
+ fun verifySearchForHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch(firstWebPage.title)
+ verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString(), searchTerm = firstWebPage.title)
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ clickClearButton()
+ // Search for invalid term
+ typeSearch("Android")
+ verifySuggestionsAreNotDisplayed(activityTestRule, firstWebPage.url.toString())
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715634
+ @Test
+ fun verifyDeletedHistoryItemsCanNotBeSearchedTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ verifyPageContent(firstWebPage.content)
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(thirdWebPage.url) {
+ verifyPageContent(thirdWebPage.content)
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ clickDeleteHistoryButton(firstWebPage.title)
+ verifyHistoryItemExists(false, firstWebPage.title)
+ clickDeleteHistoryButton(secondWebPage.title)
+ verifyHistoryItemExists(false, secondWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, firstWebPage.url.toString())
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ verifySearchEngineSuggestionResults(activityTestRule, thirdWebPage.url.toString(), searchTerm = "generic")
+ pressBack()
+ }
+ historyMenu {
+ clickDeleteHistoryButton(thirdWebPage.title)
+ verifyHistoryItemExists(false, firstWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, thirdWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903590
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ @SmokeTest
+ @Test
+ fun noHistoryInPrivateBrowsingTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(website.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHomeScreenTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHomeScreenTest.kt
new file mode 100644
index 0000000000..38ed0da774
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHomeScreenTest.kt
@@ -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.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the presence of home screen and first-run homescreen elements
+ *
+ * Note: For private browsing, navigation bar and tabs see separate test class
+ *
+ */
+
+class ComposeHomeScreenTest : TestSetup() {
+ @get:Rule(order = 0)
+ val activityTestRule =
+ AndroidComposeTestRule(
+ HomeActivityTestRule.withDefaultSettingsOverrides(
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235396
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1844580")
+ @Test
+ fun homeScreenItemsTest() {
+ homeScreen {
+ verifyHomeWordmark()
+ verifyHomePrivateBrowsingButton()
+ verifyExistingTopSitesTabs("Wikipedia")
+ verifyExistingTopSitesTabs("Top Articles")
+ verifyExistingTopSitesTabs("Google")
+ verifyCollectionsHeader()
+ verifyNoCollectionsText()
+ scrollToPocketProvokingStories()
+ verifyThoughtProvokingStories(true)
+ verifyStoriesByTopicItems()
+ verifyCustomizeHomepageButton(true)
+ verifyNavigationToolbar()
+ verifyHomeMenuButton()
+ verifyTabButton()
+ verifyTabCounter("0")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244199
+ @Test
+ fun privateBrowsingHomeScreenItemsTest() {
+ homeScreen { }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ verifyPrivateBrowsingHomeScreenItems()
+ }.openCommonMythsLink {
+ verifyUrl("common-myths-about-private-browsing")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1364362
+ @SmokeTest
+ @Test
+ fun verifyJumpBackInSectionTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isRecentlyVisitedFeatureEnabled = false
+ it.isPocketEnabled = false
+ }
+
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ verifyPageContent(firstWebPage.content)
+ verifyUrl(firstWebPage.url.toString())
+ }.goToHomescreen {
+ verifyJumpBackInSectionIsDisplayed()
+ verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
+ verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
+ verifyJumpBackInShowAllButton()
+ }.clickJumpBackInShowAllButton(activityTestRule) {
+ verifyExistingOpenTabs(firstWebPage.title)
+ }.closeTabDrawer {
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ verifyUrl(secondWebPage.url.toString())
+ }.goToHomescreen {
+ verifyJumpBackInSectionIsDisplayed()
+ verifyJumpBackInItemTitle(activityTestRule, secondWebPage.title)
+ verifyJumpBackInItemWithUrl(activityTestRule, secondWebPage.url.toString())
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTabWithTitle(secondWebPage.title)
+ }.closeTabDrawer {
+ }
+
+ homeScreen {
+ verifyJumpBackInSectionIsDisplayed()
+ verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
+ verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTab()
+ }
+
+ homeScreen {
+ verifyJumpBackInSectionIsNotDisplayed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569839
+ @Test
+ fun verifyCustomizeHomepageButtonTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.goToHomescreen {
+ }.openCustomizeHomepage {
+ clickShortcutsButton()
+ clickJumpBackInButton()
+ clickRecentBookmarksButton()
+ clickRecentSearchesButton()
+ clickPocketButton()
+ }.goBackToHomeScreen {
+ verifyCustomizeHomepageButton(false)
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ clickShortcutsButton()
+ }.goBackToHomeScreen {
+ verifyCustomizeHomepageButton(true)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeMediaNotificationTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeMediaNotificationTest.kt
new file mode 100644
index 0000000000..c673cbe3de
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeMediaNotificationTest.kt
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.notificationShade
+
+/**
+ * Tests for verifying basic functionality of media notifications:
+ * - video and audio playback system notifications appear and can pause/play the media content
+ * - a media notification icon is displayed on the homescreen for the tab playing media content
+ * Note: this test only verifies media notifications, not media itself
+ */
+class ComposeMediaNotificationTest : TestSetup() {
+ @get:Rule(order = 0)
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityTestRule.withDefaultSettingsOverrides(
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1347033
+ @SmokeTest
+ @Test
+ fun verifyVideoPlaybackSystemNotificationTest() {
+ val videoTestPage = TestAssetHelper.getVideoPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(videoTestPage.url) {
+ mDevice.waitForIdle()
+ clickPageObject(MatcherHelper.itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openNotificationShade {
+ verifySystemNotificationExists(videoTestPage.title)
+ clickMediaNotificationControlButton("Pause")
+ verifyMediaSystemNotificationButtonState("Play")
+ }
+
+ mDevice.pressBack()
+
+ browserScreen {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }.openComposeTabDrawer(composeTestRule) {
+ closeTab()
+ }
+
+ mDevice.openNotification()
+
+ notificationShade {
+ verifySystemNotificationDoesNotExist(videoTestPage.title)
+ }
+
+ // close notification shade before the next test
+ mDevice.pressBack()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2316010
+ @SmokeTest
+ @Test
+ fun verifyAudioPlaybackSystemNotificationTest() {
+ val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(audioTestPage.url) {
+ mDevice.waitForIdle()
+ clickPageObject(MatcherHelper.itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openNotificationShade {
+ verifySystemNotificationExists(audioTestPage.title)
+ clickMediaNotificationControlButton("Pause")
+ verifyMediaSystemNotificationButtonState("Play")
+ }
+
+ mDevice.pressBack()
+
+ browserScreen {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }.openComposeTabDrawer(composeTestRule) {
+ closeTab()
+ }
+
+ mDevice.openNotification()
+
+ notificationShade {
+ verifySystemNotificationDoesNotExist(audioTestPage.title)
+ }
+
+ // close notification shade before the next test
+ mDevice.pressBack()
+ }
+
+ // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903595
+ @Test
+ fun mediaSystemNotificationInPrivateModeTest() {
+ val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
+
+ homeScreen {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.toggleToPrivateTabs {
+ }.openNewTab {
+ }.submitQuery(audioTestPage.url.toString()) {
+ mDevice.waitForIdle()
+ clickPageObject(MatcherHelper.itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openNotificationShade {
+ verifySystemNotificationExists("A site is playing media")
+ clickMediaNotificationControlButton("Pause")
+ verifyMediaSystemNotificationButtonState("Play")
+ }
+
+ mDevice.pressBack()
+
+ browserScreen {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }.openComposeTabDrawer(composeTestRule) {
+ closeTab()
+ verifySnackBarText("Private tab closed")
+ }
+
+ mDevice.openNotification()
+
+ notificationShade {
+ verifySystemNotificationDoesNotExist("A site is playing media")
+ }
+
+ // close notification shade before and go back to regular mode before the next test
+ mDevice.pressBack()
+ homeScreen { }.togglePrivateBrowsingMode()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeNavigationToolbarTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeNavigationToolbarTest.kt
new file mode 100644
index 0000000000..2ff866316b
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeNavigationToolbarTest.kt
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import java.util.Locale
+
+/**
+ * Tests for verifying basic functionality of browser navigation and page related interactions
+ *
+ * Including:
+ * - Visiting a URL
+ * - Back and Forward navigation
+ * - Refresh
+ * - Find in page
+ */
+
+class ComposeNavigationToolbarTest : TestSetup() {
+ @get:Rule
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityTestRule.withDefaultSettingsOverrides(
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326
+ // Swipes the nav bar left/right to switch between tabs
+ @SmokeTest
+ @Test
+ fun swipeToSwitchTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ swipeNavBarRight(secondWebPage.url.toString())
+ verifyUrl(firstWebPage.url.toString())
+ swipeNavBarLeft(firstWebPage.url.toString())
+ verifyUrl(secondWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987327
+ // Because it requires changing system prefs, this test will run only on Debug builds
+ @Test
+ fun swipeToSwitchTabInRTLTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val arabicLocale = Locale("ar", "AR")
+
+ runWithSystemLocaleChanged(arabicLocale, composeTestRule.activityRule) {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ swipeNavBarLeft(secondWebPage.url.toString())
+ verifyUrl(firstWebPage.url.toString())
+ swipeNavBarRight(firstWebPage.url.toString())
+ verifyUrl(secondWebPage.url.toString())
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSearchTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSearchTest.kt
new file mode 100644
index 0000000000..eaa76b1481
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSearchTest.kt
@@ -0,0 +1,827 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.content.Context
+import android.hardware.camera2.CameraManager
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.core.net.toUri
+import androidx.test.espresso.Espresso
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens
+import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission
+import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
+import org.mozilla.fenix.helpers.AppAndSystemHelper.verifyKeyboardVisibility
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createBookmarkItem
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createTabItem
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.setCustomSearchEngine
+import org.mozilla.fenix.helpers.SearchDispatcher
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickContextMenuItem
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.longClickPageObject
+import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.searchScreen
+
+/**
+ * Tests for verifying the search fragment
+ *
+ * Including:
+ * - Verify the toolbar, awesomebar, and shortcut bar are displayed
+ * - Select shortcut button
+ * - Select scan button
+ *
+ */
+
+class ComposeSearchTest : TestSetup() {
+ private lateinit var searchMockServer: MockWebServer
+ private val queryString: String = "firefox"
+ private val generalEnginesList = listOf("DuckDuckGo", "Google", "Bing")
+ private val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
+
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityTestRule(
+ skipOnboarding = true,
+ isPocketEnabled = false,
+ isJumpBackInCFREnabled = false,
+ isRecentTabsFeatureEnabled = false,
+ isTCPCFREnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ searchMockServer = MockWebServer().apply {
+ dispatcher = SearchDispatcher()
+ start()
+ }
+ }
+
+ @After
+ override fun tearDown() {
+ super.tearDown()
+ searchMockServer.shutdown()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189
+ @Test
+ fun verifySearchBarItemsTest() {
+ navigationToolbar {
+ verifyDefaultSearchEngine("Google")
+ verifySearchBarPlaceholder("Search or enter address")
+ }.clickUrlbar {
+ verifyKeyboardVisibility(isExpectedToBeVisible = true)
+ verifyScanButtonVisibility(visible = true)
+ verifyVoiceSearchButtonVisibility(enabled = true)
+ verifySearchBarPlaceholder("Search or enter address")
+ typeSearch("mozilla ")
+ verifyScanButtonVisibility(visible = false)
+ verifyVoiceSearchButtonVisibility(enabled = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154190
+ @Test
+ fun verifySearchSelectorMenuItemsTest() {
+ homeScreen {
+ }.openSearch {
+ verifySearchView()
+ verifySearchToolbar(isDisplayed = true)
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(
+ "DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay",
+ "Bookmarks", "Tabs", "History", "Search settings",
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154194
+ @Test
+ fun verifySearchPlaceholderForGeneralDefaultSearchEnginesTest() {
+ generalEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ }.clickSearchEngineSettings {
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine(it)
+ exitMenu()
+ }
+ navigationToolbar {
+ verifySearchBarPlaceholder("Search or enter address")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154195
+ @Test
+ fun verifySearchPlaceholderForNotDefaultGeneralSearchEnginesTest() {
+ val generalEnginesList = listOf("DuckDuckGo", "Bing")
+
+ generalEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifySearchBarPlaceholder("Search the web")
+ }.dismissSearchBar {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154196
+ @Test
+ fun verifySearchPlaceholderForTopicSpecificSearchEnginesTest() {
+ val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
+
+ topicEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifySearchBarPlaceholder("Enter search terms")
+ }.dismissSearchBar {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1059459
+ @SmokeTest
+ @Test
+ fun verifyQRScanningCameraAccessDialogTest() {
+ val cameraManager = TestHelper.appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ Assume.assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ homeScreen {
+ }.openSearch {
+ clickScanButton()
+ denyPermission()
+ clickScanButton()
+ clickDismissPermissionRequiredDialog()
+ }
+ homeScreen {
+ }.openSearch {
+ clickScanButton()
+ clickGoToPermissionsSettings()
+ assertNativeAppOpens(Constants.PackageName.ANDROID_SETTINGS)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235397
+ @SmokeTest
+ @Test
+ fun scanQRCodeToOpenAWebpageTest() {
+ val cameraManager = TestHelper.appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ Assume.assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ homeScreen {
+ }.openSearch {
+ clickScanButton()
+ grantSystemPermission()
+ verifyScannerOpen()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154191
+ @Test
+ fun verifyScanButtonAvailableOnlyForGeneralSearchEnginesTest() {
+ generalEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifyScanButtonVisibility(visible = true)
+ }.dismissSearchBar {}
+ }
+
+ topicEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifyScanButtonVisibility(visible = false)
+ }.dismissSearchBar {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235395
+ // Verifies a temporary change of search engine from the Search shortcut menu
+ @SmokeTest
+ @Test
+ fun searchEnginesCanBeChangedTemporarilyFromSearchSelectorMenuTest() {
+ val enginesList = listOf("DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay")
+
+ enginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(it)
+ selectTemporarySearchMethod(it)
+ verifySearchEngineIcon(it)
+ }.submitQuery("mozilla ") {
+ verifyUrl("mozilla")
+ }.goToHomescreen {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233589
+ @Test
+ fun defaultSearchEnginesCanBeSetFromSearchSelectorMenuTest() {
+ searchScreen {
+ clickSearchSelectorButton()
+ }.clickSearchEngineSettings {
+ verifyToolbarText("Search")
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine("DuckDuckGo")
+ TestHelper.exitMenu()
+ }
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ verifyUrl(queryString)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522918
+ @Test
+ fun verifyClearSearchButtonTest() {
+ homeScreen {
+ }.openSearch {
+ typeSearch(queryString)
+ clickClearButton()
+ verifySearchBarPlaceholder("Search or enter address")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1623441
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @SmokeTest
+ @Test
+ fun searchResultsOpenedInNewTabsGenerateSearchGroupsTest() {
+ val searchEngineName = "TestSearchEngine"
+ // setting our custom mockWebServer search URL
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ Espresso.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592229
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun verifyAPageIsAddedToASearchGroupOnlyOnceTest() {
+ val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
+ val secondPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 2).url
+ val originPageUrl =
+ "http://localhost:${searchMockServer.port}/pages/searchResults.html?search=test%20search".toUri()
+ val searchEngineName = "TestSearchEngine"
+ // setting our custom mockWebServer search URL
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ Espresso.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ Espresso.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ Espresso.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ verifyTestPageUrl(firstPageUrl)
+ verifyTestPageUrl(secondPageUrl)
+ verifyTestPageUrl(originPageUrl)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1591782
+ @Ignore("Failing due to known bug, see https://github.com/mozilla-mobile/fenix/issues/23818")
+ @Test
+ fun searchGroupIsGeneratedWhenNavigatingInTheSameTabTest() {
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ clickPageObject(MatcherHelper.itemContainingText("Link 1"))
+ waitForPageToLoad()
+ Espresso.pressBack()
+ clickPageObject(MatcherHelper.itemContainingText("Link 2"))
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1591781
+ @SmokeTest
+ @Test
+ fun searchGroupIsNotGeneratedForLinksOpenedInPrivateTabsTest() {
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in private tab")
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in private tab")
+ }.openComposeTabDrawer(activityTestRule) {
+ }.toggleToPrivateTabs {
+ }.openPrivateTab(0) {
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openPrivateTab(1) {
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ togglePrivateBrowsingModeOnOff()
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 3)
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryItemExists(shouldExist = false, item = "3 sites")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592269
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @SmokeTest
+ @Test
+ fun deleteIndividualHistoryItemsFromSearchGroupTest() {
+ val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
+ val secondPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 2).url
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ TestHelper.mDevice.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ clickDeleteHistoryButton(firstPageUrl.toString())
+ TestHelper.longTapSelectItem(secondPageUrl)
+ multipleSelectionToolbar {
+ Espresso.openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ clickMultiSelectionDelete()
+ }
+ TestHelper.exitMenu()
+ }
+ homeScreen {
+ // checking that the group is removed when only 1 item is left
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 1)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592242
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun deleteSearchGroupFromHomeScreenTest() {
+ val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ TestHelper.mDevice.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ clickDeleteAllHistoryButton()
+ confirmDeleteAllHistory()
+ verifySnackBarText(expectedText = "Group deleted")
+ verifyHistoryItemExists(shouldExist = false, firstPageUrl.toString())
+ }.goBack {}
+ homeScreen {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifySearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592235
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun openAPageFromHomeScreenSearchGroupTest() {
+ val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
+ val secondPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 2).url
+
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ TestHelper.mDevice.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ }.openWebsite(firstPageUrl) {
+ verifyUrl(firstPageUrl.toString())
+ }.goToHomescreen {
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ TestHelper.longTapSelectItem(firstPageUrl)
+ TestHelper.longTapSelectItem(secondPageUrl)
+ Espresso.openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenNewTab(activityTestRule) {
+ verifyNormalBrowsingButtonIsSelected()
+ }.closeTabDrawer {}
+ Espresso.openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ multipleSelectionToolbar {
+ }.clickOpenPrivateTab {
+ verifyPrivateModeSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592238
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun shareAPageFromHomeScreenSearchGroupTest() {
+ val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1).url
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ TestHelper.mDevice.pressBack()
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ TestHelper.longTapSelectItem(firstPageUrl)
+ }
+
+ multipleSelectionToolbar {
+ clickShareHistoryButton()
+ verifyShareOverlay()
+ verifyShareTabFavicon()
+ verifyShareTabTitle()
+ verifyShareTabUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232633
+ // Default search code for Google-US
+ @Test
+ fun defaultSearchCodeGoogleUS() {
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.openHistory {
+ // Full URL no longer visible in the nav bar, so we'll check the history record
+ // A search group is sometimes created when searching with Google (probably redirects)
+ try {
+ verifyHistoryItemExists(shouldExist = true, Constants.searchEngineCodes["Google"]!!)
+ } catch (e: AssertionError) {
+ openSearchGroup(queryString)
+ verifyHistoryItemExists(shouldExist = true, Constants.searchEngineCodes["Google"]!!)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232637
+ // Default search code for Bing-US
+ @Test
+ fun defaultSearchCodeBingUS() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine("Bing")
+ TestHelper.exitMenu()
+ }
+
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.openHistory {
+ // Full URL no longer visible in the nav bar, so we'll check the history record
+ // A search group is sometimes created when searching with Bing (probably redirects)
+ try {
+ verifyHistoryItemExists(shouldExist = true, Constants.searchEngineCodes["Bing"]!!)
+ } catch (e: AssertionError) {
+ openSearchGroup(queryString)
+ verifyHistoryItemExists(shouldExist = true, Constants.searchEngineCodes["Bing"]!!)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232638
+ // Default search code for DuckDuckGo-US
+ @Test
+ fun defaultSearchCodeDuckDuckGoUS() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine("DuckDuckGo")
+ TestHelper.exitMenu()
+ }
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.openHistory {
+ // Full URL no longer visible in the nav bar, so we'll check the history record
+ // A search group is sometimes created when searching with DuckDuckGo
+ try {
+ verifyHistoryItemExists(shouldExist = true, item = Constants.searchEngineCodes["DuckDuckGo"]!!)
+ } catch (e: AssertionError) {
+ openSearchGroup(queryString)
+ verifyHistoryItemExists(shouldExist = true, item = Constants.searchEngineCodes["DuckDuckGo"]!!)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1850517
+ // Test that verifies the Firefox Suggest results in a general search context
+ @Test
+ fun verifyFirefoxSuggestHeaderForBrowsingDataSuggestionsTest() {
+ val firstPage = TestAssetHelper.getGenericAsset(searchMockServer, 1)
+ val secondPage = TestAssetHelper.getGenericAsset(searchMockServer, 2)
+
+ createTabItem(firstPage.url.toString())
+ createBookmarkItem(secondPage.url.toString(), secondPage.title, 1u)
+
+ homeScreen {
+ }.openSearch {
+ typeSearch("generic")
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ firstPage.url.toString(),
+ secondPage.url.toString(),
+ ),
+ searchTerm = "generic",
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154197
+ @Test
+ fun verifyTabsSearchItemsTest() {
+ navigationToolbar {
+ }.clickUrlbar {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod("Tabs")
+ verifyKeyboardVisibility(isExpectedToBeVisible = true)
+ verifyScanButtonVisibility(visible = false)
+ verifyVoiceSearchButtonVisibility(enabled = true)
+ verifySearchBarPlaceholder(text = "Search tabs")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154198
+ @Test
+ fun verifyTabsSearchWithoutOpenTabsTest() {
+ navigationToolbar {
+ }.clickUrlbar {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(searchEngineName = "Tabs")
+ typeSearch(searchTerm = "Mozilla")
+ verifySuggestionsAreNotDisplayed(rule = activityTestRule, "Mozilla")
+ clickClearButton()
+ verifySearchBarPlaceholder("Search tabs")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154199
+ @SmokeTest
+ @Test
+ fun verifyTabsSearchWithOpenTabsTest() {
+ val firstPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 1)
+ val secondPageUrl = TestAssetHelper.getGenericAsset(searchMockServer, 2)
+
+ createTabItem(firstPageUrl.url.toString())
+ createTabItem(secondPageUrl.url.toString())
+
+ navigationToolbar {
+ }.clickUrlbar {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(searchEngineName = "Tabs")
+ typeSearch(searchTerm = "Mozilla")
+ verifySuggestionsAreNotDisplayed(rule = activityTestRule, "Mozilla")
+ clickClearButton()
+ typeSearch(searchTerm = "generic")
+ verifyTypedToolbarText("generic")
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ firstPageUrl.url.toString(),
+ secondPageUrl.url.toString(),
+ ),
+ searchTerm = "generic",
+ )
+ }.clickSearchSuggestion(firstPageUrl.url.toString()) {
+ verifyTabCounter("2")
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyOpenTabsOrder(position = 1, title = firstPageUrl.url.toString())
+ verifyOpenTabsOrder(position = 2, title = secondPageUrl.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154203
+ @Test
+ fun verifyBookmarksSearchItemsTest() {
+ navigationToolbar {
+ }.clickSearchSelectorButton {
+ selectTemporarySearchMethod("Bookmarks")
+ verifySearchBarPlaceholder("Search bookmarks")
+ verifyKeyboardVisibility(isExpectedToBeVisible = true)
+ verifyScanButtonVisibility(visible = false)
+ verifyVoiceSearchButtonVisibility(enabled = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154204
+ @Test
+ fun verifyBookmarkSearchWithNoBookmarksTest() {
+ navigationToolbar {
+ }.clickSearchSelectorButton {
+ selectTemporarySearchMethod("Bookmarks")
+ typeSearch("test")
+ verifySuggestionsAreNotDisplayed(activityTestRule, "test")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154206
+ @Test
+ fun verifyBookmarksSearchForBookmarkedItemsTest() {
+ createBookmarkItem(url = "https://bookmarktest1.com", title = "Test1", position = 1u)
+ createBookmarkItem(url = "https://bookmarktest2.com", title = "Test2", position = 2u)
+
+ navigationToolbar {
+ }.clickSearchSelectorButton {
+ selectTemporarySearchMethod("Bookmarks")
+ typeSearch("test")
+ verifySearchEngineSuggestionResults(
+ activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ "Test1",
+ "https://bookmarktest1.com/",
+ "Test2",
+ "https://bookmarktest2.com/",
+ ),
+ searchTerm = "test",
+ )
+ }.dismissSearchBar {
+ }.openSearch {
+ typeSearch("mozilla ")
+ verifySuggestionsAreNotDisplayed(activityTestRule, "Test1", "Test2")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154212
+ @Test
+ fun verifyHistorySearchItemsTest() {
+ navigationToolbar {
+ }.clickUrlbar {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod("History")
+ verifyKeyboardVisibility(isExpectedToBeVisible = true)
+ verifyScanButtonVisibility(visible = false)
+ verifyVoiceSearchButtonVisibility(enabled = true)
+ verifySearchBarPlaceholder(text = "Search history")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154213
+ @Test
+ fun verifyHistorySearchWithoutBrowsingHistoryTest() {
+ navigationToolbar {
+ }.clickUrlbar {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(searchEngineName = "History")
+ typeSearch(searchTerm = "Mozilla")
+ verifySuggestionsAreNotDisplayed(rule = activityTestRule, "Mozilla")
+ clickClearButton()
+ verifySearchBarPlaceholder("Search history")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataOnQuitTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataOnQuitTest.kt
new file mode 100644
index 0000000000..21521871b3
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataOnQuitTest.kt
@@ -0,0 +1,267 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.Manifest
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.core.net.toUri
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.rule.GrantPermissionRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.downloadRobot
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the Settings for:
+ * Delete Browsing Data on quit
+ *
+ */
+class ComposeSettingsDeleteBrowsingDataOnQuitTest : TestSetup() {
+ @get:Rule(order = 0)
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(
+ skipOnboarding = true,
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ // Automatically allows app permissions, avoiding a system dialog showing up.
+ @get:Rule(order = 1)
+ val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+ Manifest.permission.RECORD_AUDIO,
+ )
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416048
+ @Test
+ fun deleteBrowsingDataOnQuitSettingTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ verifyNavigationToolBarHeader()
+ verifyDeleteBrowsingOnQuitEnabled(false)
+ verifyDeleteBrowsingOnQuitButtonSummary()
+ verifyDeleteBrowsingOnQuitEnabled(false)
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ verifyDeleteBrowsingOnQuitEnabled(true)
+ verifyAllTheCheckBoxesText()
+ verifyAllTheCheckBoxesChecked(true)
+ }.goBack {
+ verifySettingsOptionSummary("Delete browsing data on quit", "On")
+ }.goBack {
+ }.openThreeDotMenu {
+ verifyQuitButtonExists()
+ pressBack()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser("test".toUri()) {
+ }.openThreeDotMenu {
+ verifyQuitButtonExists()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416049
+ @Test
+ fun deleteOpenTabsOnQuitTest() {
+ val testPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.url) {
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ restartApp(composeTestRule.activityRule)
+ }
+ homeScreen {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNoOpenTabsInNormalBrowsing()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416050
+ @Test
+ fun deleteBrowsingHistoryOnQuitTest() {
+ val genericPage =
+ getStorageTestAsset(mockWebServer, "generic1.html")
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ restartApp(composeTestRule.activityRule)
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ exitMenu()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416051
+ @Test
+ fun deleteCookiesAndSiteDataOnQuitTest() {
+ val storageWritePage =
+ getStorageTestAsset(mockWebServer, "storage_write.html")
+ val storageCheckPage =
+ getStorageTestAsset(mockWebServer, "storage_check.html")
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage.url) {
+ clickPageObject(MatcherHelper.itemWithText("Set cookies"))
+ verifyPageContent("Values written to storage")
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ restartApp(composeTestRule.activityRule)
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(storageCheckPage.url) {
+ verifyPageContent("Session storage empty")
+ verifyPageContent("Local storage empty")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage.url) {
+ verifyPageContent("No cookies set")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1243096
+ @SmokeTest
+ @Test
+ fun deleteDownloadsOnQuitTest() {
+ val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip")
+ verifyDownloadCompleteNotificationPopup()
+ }.closeDownloadPrompt {
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ mDevice.waitForIdle()
+ }
+ restartApp(composeTestRule.activityRule)
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyEmptyDownloadsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416053
+ @SmokeTest
+ @Test
+ fun deleteSitePermissionsOnQuitTest() {
+ val testPage = "https://mozilla-mobile.github.io/testapp/permissions"
+ val testPageSubstring = "https://mozilla-mobile.github.io:443"
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton {
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Microphone not allowed")
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ mDevice.waitForIdle()
+ }
+ restartApp(composeTestRule.activityRule)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton {
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416052
+ @Test
+ fun deleteCachedFilesOnQuitTest() {
+ val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ homeScreen {
+ verifyExistingTopSitesTabs(pocketTopArticles)
+ }.openTopSiteTabWithTitle(pocketTopArticles) {
+ waitForPageToLoad()
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ mDevice.waitForIdle()
+ }
+ // disabling wifi to prevent downloads in the background
+ setNetworkEnabled(enabled = false)
+ restartApp(composeTestRule.activityRule)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser("about:cache".toUri()) {
+ verifyNetworkCacheIsEmpty("memory")
+ verifyNetworkCacheIsEmpty("disk")
+ }
+ setNetworkEnabled(enabled = true)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataTest.kt
new file mode 100644
index 0000000000..7b1fc996b2
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataTest.kt
@@ -0,0 +1,259 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.settingsScreen
+
+/**
+ * Tests for verifying the Settings for:
+ * Delete Browsing Data
+ */
+
+class ComposeSettingsDeleteBrowsingDataTest : TestSetup() {
+ @get:Rule
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(
+ skipOnboarding = true,
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/937561
+ @Test
+ fun deleteBrowsingDataOptionStatesTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyAllCheckBoxesAreChecked()
+ switchBrowsingHistoryCheckBox()
+ switchCachedFilesCheckBox()
+ verifyOpenTabsCheckBox(true)
+ verifyBrowsingHistoryDetails(false)
+ verifyCookiesCheckBox(true)
+ verifyCachedFilesCheckBox(false)
+ verifySitePermissionsCheckBox(true)
+ verifyDownloadsCheckBox(true)
+ }
+
+ restartApp(composeTestRule.activityRule)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyOpenTabsCheckBox(true)
+ verifyBrowsingHistoryDetails(false)
+ verifyCookiesCheckBox(true)
+ verifyCachedFilesCheckBox(false)
+ verifySitePermissionsCheckBox(true)
+ verifyDownloadsCheckBox(true)
+ switchOpenTabsCheckBox()
+ switchBrowsingHistoryCheckBox()
+ switchCookiesCheckBox()
+ switchCachedFilesCheckBox()
+ switchSitePermissionsCheckBox()
+ switchDownloadsCheckBox()
+ verifyOpenTabsCheckBox(false)
+ verifyBrowsingHistoryDetails(true)
+ verifyCookiesCheckBox(false)
+ verifyCachedFilesCheckBox(true)
+ verifySitePermissionsCheckBox(false)
+ verifyDownloadsCheckBox(false)
+ }
+
+ restartApp(composeTestRule.activityRule)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyOpenTabsCheckBox(false)
+ verifyBrowsingHistoryDetails(true)
+ verifyCookiesCheckBox(false)
+ verifyCachedFilesCheckBox(true)
+ verifySitePermissionsCheckBox(false)
+ verifyDownloadsCheckBox(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/517811
+ @Test
+ fun deleteOpenTabsBrowsingDataWithNoOpenTabsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyAllCheckBoxesAreChecked()
+ selectOnlyOpenTabsCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ }
+ settingsScreen {
+ verifyGeneralHeading()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/353531
+ @SmokeTest
+ @Test
+ fun deleteOpenTabsBrowsingDataTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyAllCheckBoxesAreChecked()
+ selectOnlyOpenTabsCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ clickDialogCancelButton()
+ verifyOpenTabsCheckBox(true)
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ }
+ settingsScreen {
+ verifyGeneralHeading()
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyOpenTabsDetails("0")
+ }.goBack {
+ }.goBack {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNoOpenTabsInNormalBrowsing()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/378864
+ @SmokeTest
+ @Test
+ fun deleteBrowsingHistoryTest() {
+ val genericPage = getStorageTestAsset(mockWebServer, "generic1.html").url
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage) {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyBrowsingHistoryDetails("1")
+ selectOnlyBrowsingHistoryCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ clickDialogCancelButton()
+ verifyBrowsingHistoryDetails(true)
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ verifyBrowsingHistoryDetails("0")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ mDevice.pressBack()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416041
+ @SmokeTest
+ @Test
+ fun deleteCookiesAndSiteDataTest() {
+ val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val storageWritePage = getStorageTestAsset(mockWebServer, "storage_write.html").url
+ val storageCheckPage = getStorageTestAsset(mockWebServer, "storage_check.html").url
+
+ // Browsing a generic page to allow GV to load on a fresh run
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage) {
+ verifyPageContent("No cookies set")
+ clickPageObject(itemWithResId("setCookies"))
+ verifyPageContent("user=android")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageCheckPage) {
+ verifyPageContent("Session storage has value")
+ verifyPageContent("Local storage has value")
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ selectOnlyCookiesCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ clickDialogCancelButton()
+ verifyCookiesCheckBox(status = true)
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(storageCheckPage) {
+ verifyPageContent("Session storage empty")
+ verifyPageContent("Local storage empty")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage) {
+ verifyPageContent("No cookies set")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416042
+ @SmokeTest
+ @Test
+ fun deleteCachedFilesTest() {
+ val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles)
+
+ homeScreen {
+ verifyExistingTopSitesTabs(pocketTopArticles)
+ }.openTopSiteTabWithTitle(pocketTopArticles) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery("about:cache") {
+ // disabling wifi to prevent downloads in the background
+ setNetworkEnabled(enabled = false)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ selectOnlyCachedFilesCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ exitMenu()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ verifyNetworkCacheIsEmpty("memory")
+ verifyNetworkCacheIsEmpty("disk")
+ }
+ setNetworkEnabled(enabled = true)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsPrivateBrowsingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsPrivateBrowsingTest.kt
new file mode 100644
index 0000000000..1d33882aad
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsPrivateBrowsingTest.kt
@@ -0,0 +1,163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.AppAndSystemHelper
+import org.mozilla.fenix.helpers.DataGenerationHelper
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.addToHomeScreen
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class ComposeSettingsPrivateBrowsingTest : TestSetup() {
+ private val pageShortcutName = DataGenerationHelper.generateRandomString(5)
+
+ @get:Rule
+ val activityTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(
+ skipOnboarding = true,
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/555822
+ @Test
+ fun verifyPrivateBrowsingMenuItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ verifyAddPrivateBrowsingShortcutButton()
+ verifyOpenLinksInPrivateTab()
+ verifyOpenLinksInPrivateTabOff()
+ }.goBack {
+ verifySettingsView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/420086
+ @Test
+ fun launchLinksInAPrivateTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ setOpenLinksInPrivateOn()
+
+ AppAndSystemHelper.openAppFromExternalLink(firstWebPage.url.toString())
+
+ browserScreen {
+ verifyUrl(firstWebPage.url.toString())
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyPrivateBrowsingButtonIsSelected()
+ }.closeTabDrawer {
+ }.goToHomescreen { }
+
+ setOpenLinksInPrivateOff()
+
+ // We need to open a different link, otherwise it will open the same session
+ AppAndSystemHelper.openAppFromExternalLink(secondWebPage.url.toString())
+
+ browserScreen {
+ verifyUrl(secondWebPage.url.toString())
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyNormalBrowsingButtonIsSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/555776
+ @Test
+ fun launchPageShortcutInPrivateBrowsingTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ setOpenLinksInPrivateOn()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.openAddToHomeScreen {
+ addShortcutName(pageShortcutName)
+ clickAddShortcutButton()
+ clickAddAutomaticallyButton()
+ verifyShortcutAdded(pageShortcutName)
+ }
+
+ TestHelper.mDevice.waitForIdle()
+ // We need to close the existing tab here, to open a different session
+ TestHelper.restartApp(activityTestRule.activityRule)
+
+ browserScreen {
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyNormalBrowsingButtonIsSelected()
+ closeTab()
+ }
+
+ addToHomeScreen {
+ }.searchAndOpenHomeScreenShortcut(pageShortcutName) {
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyPrivateBrowsingButtonIsSelected()
+ closeTab()
+ }
+
+ setOpenLinksInPrivateOff()
+
+ addToHomeScreen {
+ }.searchAndOpenHomeScreenShortcut(pageShortcutName) {
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyNormalBrowsingButtonIsSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414583
+ @Test
+ fun addPrivateBrowsingShortcutFromSettingsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ cancelPrivateShortcutAddition()
+ addPrivateShortcutToHomescreen()
+ verifyPrivateBrowsingShortcutIcon()
+ }.openPrivateBrowsingShortcut {
+ verifySearchView()
+ }.openBrowser {
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyPrivateBrowsingButtonIsSelected()
+ }
+ }
+}
+
+private fun setOpenLinksInPrivateOn() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ verifyOpenLinksInPrivateTabEnabled()
+ clickOpenLinksInPrivateTabSwitch()
+ }.goBack {
+ }.goBack {
+ verifyHomeComponent()
+ }
+}
+
+private fun setOpenLinksInPrivateOff() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ clickOpenLinksInPrivateTabSwitch()
+ verifyOpenLinksInPrivateTabOff()
+ }.goBack {
+ }.goBack {
+ verifyHomeComponent()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt
new file mode 100644
index 0000000000..7ef1513532
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt
@@ -0,0 +1,525 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.closeApp
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.notificationShade
+
+/**
+ * Tests for verifying basic functionality of tabbed browsing
+ *
+ * Including:
+ * - Opening a tab
+ * - Opening a private tab
+ * - Verifying tab list
+ * - Closing all tabs
+ * - Close tab
+ * - Swipe to close tab (temporarily disabled)
+ * - Undo close tab
+ * - Close private tabs persistent notification
+ * - Empty tab tray state
+ * - Tab tray details
+ * - Shortcut context menu navigation
+ */
+
+class ComposeTabbedBrowsingTest : TestSetup() {
+ @get:Rule(order = 0)
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(
+ skipOnboarding = true,
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903599
+ @Test
+ fun closeAllTabsTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNormalTabsList()
+ }.openThreeDotMenu {
+ verifyCloseAllTabsButton()
+ verifyShareAllTabsButton()
+ verifySelectTabsButton()
+ }.closeAllTabs {
+ verifyTabCounter("0")
+ }
+
+ // Repeat for Private Tabs
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyPrivateTabsList()
+ }.openThreeDotMenu {
+ verifyCloseAllTabsButton()
+ }.closeAllTabs {
+ verifyTabCounter("0")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2349580
+ @Test
+ fun closingTabsTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyExistingOpenTabs("Test_Page_1")
+ closeTab()
+ verifySnackBarText("Tab closed")
+ clickSnackbarButton("UNDO")
+ }
+ browserScreen {
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903604
+ @Test
+ fun swipeToCloseTabsTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyExistingOpenTabs("Test_Page_1")
+ swipeTabRight("Test_Page_1")
+ verifySnackBarText("Tab closed")
+ }
+ homeScreen {
+ verifyTabCounter("0")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyExistingOpenTabs("Test_Page_1")
+ swipeTabLeft("Test_Page_1")
+ verifySnackBarText("Tab closed")
+ }
+ homeScreen {
+ verifyTabCounter("0")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903591
+ @Test
+ fun closingPrivateTabsTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen { }.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyExistingOpenTabs("Test_Page_1")
+ closeTab()
+ verifySnackBarText("Private tab closed")
+ clickSnackbarButton("UNDO")
+ }
+ browserScreen {
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903606
+ @SmokeTest
+ @Test
+ fun tabMediaControlButtonTest() {
+ val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(audioTestPage.url) {
+ mDevice.waitForIdle()
+ clickPageObject(MatcherHelper.itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyTabMediaControlButtonState("Pause")
+ clickTabMediaControlButton("Pause")
+ verifyTabMediaControlButtonState("Play")
+ }.openTab(audioTestPage.title) {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903592
+ @SmokeTest
+ @Test
+ fun verifyCloseAllPrivateTabsNotificationTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.openNotification()
+ }
+
+ notificationShade {
+ verifyPrivateTabsNotification()
+ }.clickClosePrivateTabsNotification {
+ verifyHomeScreen()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903602
+ @Test
+ fun verifyTabTrayNotShowingStateHalfExpanded() {
+ homeScreen {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNoOpenTabsInNormalBrowsing()
+ // With no tabs opened the state should be STATE_COLLAPSED.
+ verifyTabsTrayBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
+ // Need to ensure the halfExpandedRatio is very small so that when in STATE_HALF_EXPANDED
+ // the tabTray will actually have a very small height (for a very short time) akin to being hidden.
+ verifyMinusculeHalfExpandedRatio()
+ }.clickTopBar {
+ }.waitForTabTrayBehaviorToIdle {
+ // Touching the topBar would normally advance the tabTray to the next state.
+ // We don't want that.
+ verifyTabsTrayBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
+ }.advanceToHalfExpandedState {
+ }.waitForTabTrayBehaviorToIdle {
+ // TabTray should not be displayed in STATE_HALF_EXPANDED.
+ // When advancing to this state it should immediately be hidden.
+ verifyTabTrayIsClosed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903600
+ @Test
+ fun verifyEmptyTabTray() {
+ homeScreen {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNormalBrowsingButtonIsSelected()
+ verifyPrivateBrowsingButtonIsSelected(false)
+ verifySyncedTabsButtonIsSelected(false)
+ verifyNoOpenTabsInNormalBrowsing()
+ verifyFab()
+ verifyThreeDotButton()
+ }.openThreeDotMenu {
+ verifyTabSettingsButton()
+ verifyRecentlyClosedTabsButton()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903585
+ @Test
+ fun verifyEmptyPrivateTabsTrayTest() {
+ homeScreen {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.toggleToPrivateTabs {
+ verifyNormalBrowsingButtonIsSelected(false)
+ verifyPrivateBrowsingButtonIsSelected(true)
+ verifySyncedTabsButtonIsSelected(false)
+ verifyNoOpenTabsInPrivateBrowsing()
+ verifyFab()
+ verifyThreeDotButton()
+ }.openThreeDotMenu {
+ verifyTabSettingsButton()
+ verifyRecentlyClosedTabsButton()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903601
+ @Test
+ fun verifyTabsTrayWithOpenTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNormalBrowsingButtonIsSelected()
+ verifyPrivateBrowsingButtonIsSelected(isSelected = false)
+ verifySyncedTabsButtonIsSelected(isSelected = false)
+ verifyThreeDotButton()
+ verifyNormalTabCounter()
+ verifyNormalTabsList()
+ verifyFab()
+ verifyTabThumbnail()
+ verifyExistingOpenTabs(defaultWebPage.title)
+ verifyTabCloseButton()
+ }.openTab(defaultWebPage.title) {
+ verifyUrl(defaultWebPage.url.toString())
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903587
+ @SmokeTest
+ @Test
+ fun verifyPrivateTabsTrayWithOpenTabTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.toggleToPrivateTabs {
+ }.openNewTab {
+ }.submitQuery(website.url.toString()) {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNormalBrowsingButtonIsSelected(false)
+ verifyPrivateBrowsingButtonIsSelected(true)
+ verifySyncedTabsButtonIsSelected(false)
+ verifyThreeDotButton()
+ verifyNormalTabCounter()
+ verifyPrivateTabsList()
+ verifyExistingOpenTabs(website.title)
+ verifyTabCloseButton()
+ verifyTabThumbnail()
+ verifyFab()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927314
+ @Test
+ fun tabsCounterShortcutMenuCloseTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ waitForPageToLoad()
+ }.goToHomescreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ waitForPageToLoad()
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ verifyTabButtonShortcutMenuItems()
+ }.closeTabFromShortcutsMenu {
+ browserScreen {
+ verifyTabCounter("1")
+ verifyPageContent(firstWebPage.content)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343663
+ @Test
+ fun tabsCounterShortcutMenuNewPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {}
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewPrivateTabFromShortcutsMenu {
+ verifySearchBarPlaceholder("Search or enter address")
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343662
+ @Test
+ fun tabsCounterShortcutMenuNewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {}
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewTabFromShortcutsMenu {
+ verifySearchBarPlaceholder("Search or enter address")
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927315
+ @Test
+ fun privateTabsCounterShortcutMenuCloseTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ waitForPageToLoad()
+ }.goToHomescreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ waitForPageToLoad()
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ verifyTabButtonShortcutMenuItems()
+ }.closeTabFromShortcutsMenu {
+ browserScreen {
+ verifyTabCounter("1")
+ verifyPageContent(firstWebPage.content)
+ }
+ }.openTabButtonShortcutsMenu {
+ }.closeTabFromShortcutsMenu {
+ homeScreen {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344199
+ @Test
+ fun privateTabsCounterShortcutMenuNewPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewPrivateTabFromShortcutsMenu {
+ verifySearchBarPlaceholder("Search or enter address")
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344198
+ @Test
+ fun privateTabsCounterShortcutMenuNewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewTabFromShortcutsMenu {
+ verifySearchToolbar(isDisplayed = true)
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1046683
+ @Test
+ fun verifySyncedTabsWhenUserIsNotSignedInTest() {
+ navigationToolbar {
+ }.openComposeTabDrawer(composeTestRule) {
+ verifySyncedTabsButtonIsSelected(isSelected = false)
+ }.toggleToSyncedTabs {
+ verifySyncedTabsButtonIsSelected(isSelected = true)
+ verifySyncedTabsListWhenUserIsNotSignedIn()
+ }.clickSignInToSyncButton {
+ verifyTurnOnSyncMenu()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903598
+ @SmokeTest
+ @Test
+ fun shareTabsFromTabsTrayTest() {
+ val firstWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val firstWebsiteTitle = firstWebsite.title
+ val secondWebsiteTitle = secondWebsite.title
+ val sharingApp = "Gmail"
+ val sharedUrlsString = "${firstWebsite.url}\n\n${secondWebsite.url}"
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebsite.url) {
+ verifyPageContent(firstWebsite.content)
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebsite.url.toString()) {
+ verifyPageContent(secondWebsite.content)
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyExistingOpenTabs("Test_Page_1")
+ verifyExistingOpenTabs("Test_Page_2")
+ }.openThreeDotMenu {
+ verifyShareAllTabsButton()
+ }.clickShareAllTabsButton {
+ verifyShareTabsOverlay(firstWebsiteTitle, secondWebsiteTitle)
+ verifySharingWithSelectedApp(
+ sharingApp,
+ sharedUrlsString,
+ "$firstWebsiteTitle, $secondWebsiteTitle",
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/526244
+ @Test
+ fun privateModeStaysAsDefaultAfterRestartTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.goToHomescreen {
+ }.togglePrivateBrowsingMode()
+
+ closeApp(composeTestRule.activityRule)
+ restartApp(composeTestRule.activityRule)
+
+ homeScreen {
+ verifyPrivateBrowsingHomeScreenItems()
+ }.openComposeTabDrawer(composeTestRule) {
+ }.toggleToNormalTabs {
+ verifyExistingOpenTabs(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2228470
+ @SmokeTest
+ @Test
+ fun privateTabsDoNotPersistAfterClosingAppTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openComposeTabDrawer(composeTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ }
+ closeApp(composeTestRule.activityRule)
+ restartApp(composeTestRule.activityRule)
+ homeScreen {
+ verifyPrivateBrowsingHomeScreenItems()
+ }.openComposeTabDrawer(composeTestRule) {
+ verifyNoOpenTabsInPrivateBrowsing()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTopSitesTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTopSitesTest.kt
new file mode 100644
index 0000000000..16ea4b1275
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTopSitesTest.kt
@@ -0,0 +1,253 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.homeScreenWithComposeTopSites
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests Top Sites functionality
+ *
+ * - Verifies 'Add to Firefox Home' UI functionality
+ * - Verifies 'Top Sites' context menu UI functionality
+ * - Verifies 'Top Site' usage UI functionality
+ * - Verifies existence of default top sites available on the home-screen
+ */
+
+class ComposeTopSitesTest : TestSetup() {
+ @get:Rule
+ val composeTestRule =
+ AndroidComposeTestRule(
+ HomeActivityTestRule.withDefaultSettingsOverrides(
+ composeTopSitesEnabled = true,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532598
+ @SmokeTest
+ @Test
+ fun addAWebsiteAsATopSiteTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532599
+ @Test
+ fun openTopSiteInANewTabTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openTopSiteTabWithTitle(title = defaultWebPage.title) {
+ verifyUrl(defaultWebPage.url.toString().replace("http://", ""))
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }
+
+ // Dismiss context menu popup
+ mDevice.pressBack()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532600
+ @Test
+ fun openTopSiteInANewPrivateTabTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.openTopSiteInPrivate() {
+ verifyCurrentPrivateSession(composeTestRule.activity.applicationContext)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1110321
+ @Test
+ fun renameATopSiteTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+ val newPageTitle = generateRandomString(5)
+
+ homeScreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.renameTopSite(newPageTitle) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(newPageTitle)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532601
+ @Test
+ fun removeTopSiteUsingMenuButtonTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.removeTopSite {
+ clickSnackbarButton("UNDO")
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.removeTopSite {
+ verifyNotExistingTopSiteItem(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2323641
+ @Test
+ fun removeTopSiteFromMainMenuTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openTopSiteTabWithTitle(defaultWebPage.title) {
+ }.openThreeDotMenu {
+ verifyRemoveFromShortcutsButton()
+ }.clickRemoveFromShortcuts {
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyNotExistingTopSiteItem(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561582
+ // Expected for en-us defaults
+ @Test
+ fun verifyENLocalesDefaultTopSitesListTest() {
+ homeScreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ val topSitesTitles = arrayListOf("Google", "Top Articles", "Wikipedia")
+ topSitesTitles.forEach { value ->
+ verifyExistingTopSiteItem(value)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1050642
+ @SmokeTest
+ @Test
+ fun addAndRemoveMostViewedTopSiteTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ for (i in 0..1) {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }
+ }
+
+ browserScreen {
+ }.goToHomescreenWithComposeTopSites(composeTestRule) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSiteItem(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ }.removeTopSite {
+ verifySnackBarText(getStringResource(R.string.snackbar_top_site_removed))
+ waitUntilSnackbarGone()
+ }
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt
new file mode 100644
index 0000000000..80f2d2a75c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
+import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickContextMenuItem
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.downloadRobot
+import org.mozilla.fenix.ui.robots.longClickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.shareOverlay
+
+/**
+ * Tests for verifying basic functionality of content context menus
+ *
+ * - Verifies long click "Open link in new tab" UI and functionality
+ * - Verifies long click "Open link in new Private tab" UI and functionality
+ * - Verifies long click "Copy Link" UI and functionality
+ * - Verifies long click "Share Link" UI and functionality
+ * - Verifies long click "Open image in new tab" UI and functionality
+ * - Verifies long click "Save Image" UI and functionality
+ * - Verifies long click "Copy image location" UI and functionality
+ * - Verifies long click items of mixed hypertext items
+ *
+ */
+
+class ContextMenusTest : TestSetup() {
+
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule(isJumpBackInCFREnabled = false)
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243837
+ @Test
+ fun verifyOpenLinkNewTabContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 1"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Open link in new tab")
+ verifySnackBarText("New tab opened")
+ clickSnackbarButton("SWITCH")
+ verifyUrl(genericURL.url.toString())
+ }.openTabDrawer {
+ verifyNormalModeSelected()
+ verifyExistingOpenTabs("Test_Page_1")
+ verifyExistingOpenTabs("Test_Page_4")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244655
+ @Test
+ fun verifyOpenLinkInNewPrivateTabContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 2"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Open link in private tab")
+ verifySnackBarText("New private tab opened")
+ clickSnackbarButton("SWITCH")
+ verifyUrl(genericURL.url.toString())
+ }.openTabDrawer {
+ verifyPrivateModeSelected()
+ verifyExistingOpenTabs("Test_Page_2")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243832
+ @Test
+ fun verifyCopyLinkContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 3"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Copy link")
+ verifySnackBarText("Link copied to clipboard")
+ }.openNavigationToolbar {
+ }.visitLinkFromClipboard {
+ verifyUrl(genericURL.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243838
+ @Test
+ fun verifyShareLinkContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 1"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ clickContextMenuItem("Share link")
+ shareOverlay {
+ verifyShareLinkIntent(genericURL.url)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243833
+ @Test
+ fun verifyOpenImageNewTabContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ clickContextMenuItem("Open image in new tab")
+ verifySnackBarText("New tab opened")
+ clickSnackbarButton("SWITCH")
+ verifyUrl(imageResource.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243834
+ @Test
+ fun verifyCopyImageLocationContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ clickContextMenuItem("Copy image location")
+ verifySnackBarText("Link copied to clipboard")
+ }.openNavigationToolbar {
+ }.visitLinkFromClipboard {
+ verifyUrl(imageResource.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243835
+ @Test
+ fun verifySaveImageContextMenuOptionTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ clickContextMenuItem("Save image")
+ }
+
+ downloadRobot {
+ verifyDownloadCompleteNotificationPopup()
+ }.clickOpen("image/jpeg") {} // verify open intent is matched with associated data type
+ downloadRobot {
+ verifyPhotosAppOpens()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/352050
+ @Test
+ fun verifyContextMenuLinkVariationsTest() {
+ val pageLinks =
+ TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val imageResource =
+ TestAssetHelper.getImageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pageLinks.url) {
+ mDevice.waitForIdle()
+ longClickPageObject(itemWithText("Link 1"))
+ verifyContextMenuForLocalHostLinks(genericURL.url)
+ dismissContentContextMenu()
+ longClickPageObject(itemWithText("test_link_image"))
+ verifyLinkImageContextMenuItems(imageResource.url)
+ dismissContentContextMenu()
+ longClickPageObject(itemWithText("test_no_link_image"))
+ verifyNoLinkImageContextMenuItems(imageResource.url)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2333840
+ @Test
+ fun verifyPDFContextMenuLinkVariationsTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ waitForPageToLoad()
+ longClickPageObject(itemWithText("Wikipedia link"))
+ verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
+ dismissContentContextMenu()
+ // Some options are missing from the linked and non liked images context menus in PDF files
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1012805 for more details
+ longClickPDFImage()
+ verifyContextMenuForLinksToOtherHosts("wikipedia.org".toUri())
+ dismissContentContextMenu()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/832094
+ @Test
+ fun verifyOpenLinkInAppContextMenuOptionTest() {
+ val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ longClickPageObject(itemContainingText("Youtube full link"))
+ verifyContextMenuForLinksToOtherApps("youtube.com")
+ clickContextMenuItem("Open link in external app")
+ assertExternalAppOpens(YOUTUBE_APP)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerBlockerTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerBlockerTest.kt
new file mode 100644
index 0000000000..cc9fff3d17
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerBlockerTest.kt
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the new Cookie banner blocker option and functionality.
+ */
+class CookieBannerBlockerTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2419260
+ @SmokeTest
+ @Test
+ fun verifyCookieBannerBlockerSettingsOptionTest() {
+ runWithCondition(appContext.settings().shouldUseCookieBannerPrivateMode) {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyCookieBannerBlockerButton(enabled = true)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2419273
+ @SmokeTest
+ @Test
+ fun verifyCFRAfterBlockingTheCookieBanner() {
+ runWithCondition(appContext.settings().shouldUseCookieBannerPrivateMode) {
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser("voetbal24.be".toUri()) {
+ waitForPageToLoad()
+ verifyCookieBannerExists(exists = false)
+ verifyCookieBannerBlockerCFRExists(exists = true)
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CrashReportingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CrashReportingTest.kt
new file mode 100644
index 0000000000..be99ebe8af
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CrashReportingTest.kt
@@ -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.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class CrashReportingTest : TestSetup() {
+ private val tabCrashMessage = getStringResource(R.string.tab_crash_title_2)
+
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityIntentTestRule(
+ isPocketEnabled = false,
+ isJumpBackInCFREnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ isTCPCFREnabled = false,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/308906
+ @Test
+ fun closeTabFromCrashedTabReporterTest() {
+ homeScreen {
+ }.openNavigationToolbar {
+ }.openTabCrashReporter {
+ }.clickTabCrashedCloseButton {
+ }.openTabDrawer {
+ verifyNoOpenTabsInNormalBrowsing()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2336134
+ @Ignore("Test failure caused by: https://github.com/mozilla-mobile/fenix/issues/19964")
+ @Test
+ fun restoreTabFromTabCrashedReporterTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(website.url) {}
+
+ navigationToolbar {
+ }.openTabCrashReporter {
+ clickPageObject(itemWithResId("$packageName:id/restoreTabButton"))
+ verifyPageContent(website.content)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1681928
+ @SmokeTest
+ @Test
+ fun useAppWhileTabIsCrashedTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ waitForPageToLoad()
+ }
+
+ navigationToolbar {
+ }.openTabCrashReporter {
+ verifyPageContent(tabCrashMessage)
+ }.openTabDrawer {
+ verifyExistingOpenTabs(firstWebPage.title)
+ verifyExistingOpenTabs("about:crashcontent")
+ }.closeTabDrawer {
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ }.openThreeDotMenu {
+ verifySettingsButton()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CreditCardAutofillTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CreditCardAutofillTest.kt
new file mode 100644
index 0000000000..11dac5a607
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CreditCardAutofillTest.kt
@@ -0,0 +1,589 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.bringAppToForeground
+import org.mozilla.fenix.helpers.AppAndSystemHelper.putAppToBackground
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import java.time.LocalDate
+
+class CreditCardAutofillTest : TestSetup() {
+ object MockCreditCard1 {
+ const val MOCK_CREDIT_CARD_NUMBER = "5555555555554444"
+ const val MOCK_LAST_CARD_DIGITS = "4444"
+ const val MOCK_NAME_ON_CARD = "Mastercard"
+ const val MOCK_EXPIRATION_MONTH = "February"
+ val MOCK_EXPIRATION_YEAR = (LocalDate.now().year + 1).toString()
+ val MOCK_EXPIRATION_MONTH_AND_YEAR = "02/${(LocalDate.now().year + 1)}"
+ }
+
+ object MockCreditCard2 {
+ const val MOCK_CREDIT_CARD_NUMBER = "2720994326581252"
+ const val MOCK_LAST_CARD_DIGITS = "1252"
+ const val MOCK_NAME_ON_CARD = "Mastercard"
+ const val MOCK_EXPIRATION_MONTH = "March"
+ val MOCK_EXPIRATION_YEAR = (LocalDate.now().year + 2).toString()
+ val MOCK_EXPIRATION_MONTH_AND_YEAR = "03/${(LocalDate.now().year + 2)}"
+ }
+
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512792
+ @SmokeTest
+ @Test
+ fun verifyCreditCardAutofillTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ // Opening Manage cards to dismiss here the Secure your credit prompt
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ }.goBackToAutofillSettings {
+ }.goBack {
+ }.goBack {
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ clickPageObject(itemWithResId("$packageName:id/select_credit_card_header"))
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/credit_card_number",
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ ),
+ )
+ verifyAutofilledCreditCard(MockCreditCard1.MOCK_CREDIT_CARD_NUMBER)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512798
+ @SmokeTest
+ @Test
+ fun deleteSavedCreditCardUsingToolbarButtonTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ clickSavedCreditCard()
+ clickDeleteCreditCardToolbarButton()
+ clickCancelDeleteCreditCardButton()
+ verifyEditCreditCardToolbarTitle()
+ clickDeleteCreditCardToolbarButton()
+ clickConfirmDeleteCreditCardButton()
+ verifyAddCreditCardsButton()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2271192
+ @SmokeTest
+ @Test
+ fun deleteSavedCreditCardUsingMenuButtonTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ clickSavedCreditCard()
+ clickDeleteCreditCardMenuButton()
+ clickCancelDeleteCreditCardButton()
+ verifyEditCreditCardToolbarTitle()
+ clickDeleteCreditCardMenuButton()
+ clickConfirmDeleteCreditCardButton()
+ verifyAddCreditCardsButton()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512788
+ @Test
+ fun verifyCreditCardsSectionTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1859917
+ @Test
+ fun verifyManageCreditCardsPromptOptionTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ clickPageObject(itemWithResId("$packageName:id/select_credit_card_header"))
+ }.clickManageCreditCardsButton {
+ }.goBackToBrowser {
+ verifySelectCreditCardPromptExists(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512790
+ @Test
+ fun verifyCreditCardsAutofillToggleTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ verifySelectCreditCardPromptExists(true)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ clickSaveAndAutofillCreditCardsOption()
+ verifyCreditCardsAutofillSection(false, true)
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ verifySelectCreditCardPromptExists(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512795
+ @Test
+ fun verifyEditCardsViewTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ clickSavedCreditCard()
+ verifyEditCreditCardView(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ }.goBackToSavedCreditCards {
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512796
+ @Test
+ fun verifyEditedCardIsSavedTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ clickSavedCreditCard()
+ fillAndSaveCreditCard(
+ MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard2.MOCK_NAME_ON_CARD,
+ MockCreditCard2.MOCK_EXPIRATION_MONTH,
+ MockCreditCard2.MOCK_EXPIRATION_YEAR,
+ )
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ clickPageObject(itemWithResId("$packageName:id/select_credit_card_header"))
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/credit_card_number",
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ ),
+ )
+ verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512797
+ @Test
+ fun verifyCreditCardCannotBeSavedWithoutCardNumberOrNameTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ clickSavedCreditCard()
+ clearCreditCardNumber()
+ clickSaveCreditCardToolbarButton()
+ verifyEditCreditCardToolbarTitle()
+ verifyCreditCardNumberErrorMessage()
+ }.goBackToSavedCreditCards {
+ clickSavedCreditCard()
+ clearNameOnCreditCard()
+ clickSaveCreditCardToolbarButton()
+ verifyEditCreditCardToolbarTitle()
+ verifyNameOnCreditCardErrorMessage()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512794
+ @Test
+ fun verifyMultipleCreditCardsCanBeAddedTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard2.MOCK_NAME_ON_CARD,
+ MockCreditCard2.MOCK_EXPIRATION_MONTH,
+ MockCreditCard2.MOCK_EXPIRATION_YEAR,
+ )
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ verifySavedCreditCardsSection(
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard2.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ clickPageObject(itemWithResId("$packageName:id/select_credit_card_header"))
+ verifyCreditCardSuggestion(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ )
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/credit_card_number",
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ ),
+ )
+ verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2271304
+ @Test
+ fun verifyDoNotSaveCreditCardFromPromptTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ clickPageObject(itemWithResId("$packageName:id/save_cancel"))
+ verifyUpdateOrSaveCreditCardPromptExists(exists = false)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1779194
+ @Test
+ fun verifySaveCreditCardFromPromptTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ clickPageObject(itemWithResId("$packageName:id/save_confirm"))
+ verifyUpdateOrSaveCreditCardPromptExists(exists = false)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, true)
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2271305
+ @Test
+ fun verifyCancelCreditCardUpdatePromptTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard2.MOCK_NAME_ON_CARD,
+ MockCreditCard2.MOCK_EXPIRATION_MONTH,
+ MockCreditCard2.MOCK_EXPIRATION_YEAR,
+ )
+ // Opening Manage cards to dismiss here the Secure your credit prompt
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ clickPageObject(itemWithResId("$packageName:id/select_credit_card_header"))
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/credit_card_number",
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ ),
+ )
+ verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
+ changeCreditCardExpiryDate(MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR)
+ clickCreditCardFormSubmitButton()
+ clickPageObject(itemWithResId("$packageName:id/save_cancel"))
+ verifyUpdateOrSaveCreditCardPromptExists(false)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, true)
+ clickManageSavedCreditCardsButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard2.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1779195
+ @Test
+ fun verifyConfirmCreditCardUpdatePromptTest() {
+ val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard2.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard2.MOCK_NAME_ON_CARD,
+ MockCreditCard2.MOCK_EXPIRATION_MONTH,
+ MockCreditCard2.MOCK_EXPIRATION_YEAR,
+ )
+ // Opening Manage cards to dismiss here the Secure your credit prompt
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(creditCardFormPage.url) {
+ clickCreditCardNumberTextBox()
+ clickPageObject(itemWithResId("$packageName:id/select_credit_card_header"))
+ clickPageObject(
+ itemWithResIdContainingText(
+ "$packageName:id/credit_card_number",
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ ),
+ )
+ verifyAutofilledCreditCard(MockCreditCard2.MOCK_CREDIT_CARD_NUMBER)
+ changeCreditCardExpiryDate(MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR)
+ clickCreditCardFormSubmitButton()
+ clickPageObject(itemWithResId("$packageName:id/save_confirm"))
+ verifyUpdateOrSaveCreditCardPromptExists(false)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, true)
+ clickManageSavedCreditCardsButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard2.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512791
+ @Test
+ fun verifyCreditCardRedirectionsToAutofillSectionAfterInterruptionTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyCreditCardsAutofillSection(true, false)
+ clickAddCreditCardButton()
+ fillAndSaveCreditCard(
+ MockCreditCard1.MOCK_CREDIT_CARD_NUMBER,
+ MockCreditCard1.MOCK_NAME_ON_CARD,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH,
+ MockCreditCard1.MOCK_EXPIRATION_YEAR,
+ )
+ clickManageSavedCreditCardsButton()
+ clickSecuredCreditCardsLaterButton()
+ clickSavedCreditCard()
+ putAppToBackground()
+ bringAppToForeground()
+ verifyAutofillToolbarTitle()
+ clickManageSavedCreditCardsButton()
+ verifySavedCreditCardsSection(
+ MockCreditCard1.MOCK_LAST_CARD_DIGITS,
+ MockCreditCard1.MOCK_EXPIRATION_MONTH_AND_YEAR,
+ )
+ putAppToBackground()
+ bringAppToForeground()
+ verifyAutofillToolbarTitle()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CustomTabsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CustomTabsTest.kt
new file mode 100644
index 0000000000..1e2aa93e83
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/CustomTabsTest.kt
@@ -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.ui
+
+import androidx.core.net.toUri
+import androidx.test.rule.ActivityTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.IntentReceiverActivity
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.openAppFromExternalLink
+import org.mozilla.fenix.helpers.DataGenerationHelper.createCustomTabIntent
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.customTabScreen
+import org.mozilla.fenix.ui.robots.enhancedTrackingProtection
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.notificationShade
+import org.mozilla.fenix.ui.robots.openEditURLView
+import org.mozilla.fenix.ui.robots.searchScreen
+
+class CustomTabsTest : TestSetup() {
+ private val customMenuItem = "TestMenuItem"
+ private val customTabActionButton = "CustomActionButton"
+
+ /* Updated externalLinks.html to v2.0,
+ changed the hypertext reference to mozilla-mobile.github.io/testapp/downloads for "External link"
+ */
+ private val externalLinksPWAPage = "https://mozilla-mobile.github.io/testapp/v2.0/externalLinks.html"
+ private val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ @get: Rule
+ val intentReceiverActivityTestRule = ActivityTestRule(
+ IntentReceiverActivity::class.java,
+ true,
+ false,
+ )
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/249659
+ @SmokeTest
+ @Test
+ fun verifyLoginSaveInCustomTabTest() {
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ loginPage.toUri().toString(),
+ customMenuItem,
+ ),
+ )
+
+ customTabScreen {
+ waitForPageToLoad()
+ fillAndSubmitLoginCredentials("mozilla", "firefox")
+ }
+
+ browserScreen {
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }
+
+ openAppFromExternalLink(loginPage)
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ verifySecurityPromptForLogins()
+ tapSetupLater()
+ verifySavedLoginsSectionUsername("mozilla")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334762
+ @Test
+ fun copyCustomTabToolbarUrlTest() {
+ val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ customTabPage.url.toString(),
+ customMenuItem,
+ ),
+ )
+
+ customTabScreen {
+ longCLickAndCopyToolbarUrl()
+ }
+
+ openAppFromExternalLink(customTabPage.url.toString())
+
+ navigationToolbar {
+ openEditURLView()
+ }
+
+ searchScreen {
+ clickClearButton()
+ longClickToolbar()
+ clickPasteText()
+ verifyTypedToolbarText(customTabPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334761
+ @SmokeTest
+ @Test
+ fun verifyDownloadInACustomTabTest() {
+ val customTabPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
+ val downloadFile = "web_icon.png"
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ customTabPage.toUri().toString(),
+ customMenuItem,
+ ),
+ )
+
+ customTabScreen {
+ waitForPageToLoad()
+ }
+
+ browserScreen {
+ }.clickDownloadLink(downloadFile) {
+ verifyDownloadPrompt(downloadFile)
+ }.clickDownload {
+ verifyDownloadCompleteNotificationPopup()
+ }
+ mDevice.openNotification()
+ notificationShade {
+ verifySystemNotificationExists("Download completed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/249644
+ // Verifies the main menu of a custom tab with a custom menu item
+ @SmokeTest
+ @Test
+ fun verifyCustomTabMenuItemsTest() {
+ val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ customTabPage.url.toString(),
+ customMenuItem,
+ ),
+ )
+
+ customTabScreen {
+ verifyCustomTabCloseButton()
+ }.openMainMenu {
+ verifyPoweredByTextIsDisplayed()
+ verifyCustomMenuItem(customMenuItem)
+ verifyDesktopSiteButtonExists()
+ verifyFindInPageButtonExists()
+ verifyOpenInBrowserButtonExists()
+ verifyBackButtonExists()
+ verifyForwardButtonExists()
+ verifyRefreshButtonExists()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/249645
+ // The test opens a link in a custom tab then sends it to the browser
+ @SmokeTest
+ @Test
+ fun openCustomTabInFirefoxTest() {
+ val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ customTabPage.url.toString(),
+ ),
+ )
+
+ customTabScreen {
+ verifyCustomTabCloseButton()
+ }.openMainMenu {
+ }.clickOpenInBrowserButton {
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2239548
+ @Test
+ fun shareCustomTabUsingToolbarButtonTest() {
+ val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ customTabPage.url.toString(),
+ ),
+ )
+
+ customTabScreen {
+ }.clickShareButton {
+ verifyShareTabLayout()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/249643
+ @Test
+ fun verifyCustomTabViewItemsTest() {
+ val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ pageUrl = customTabPage.url.toString(),
+ customActionButtonDescription = customTabActionButton,
+ ),
+ )
+
+ customTabScreen {
+ verifyCustomTabCloseButton()
+ verifyCustomTabsSiteInfoButton()
+ verifyCustomTabToolbarTitle(customTabPage.title)
+ verifyCustomTabUrl(customTabPage.url.toString())
+ verifyCustomTabActionButton(customTabActionButton)
+ verifyCustomTabsShareButton()
+ verifyMainMenuButton()
+ clickCustomTabCloseButton()
+ }
+ homeScreen {
+ verifyHomeScreenAppBarItems()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2239544
+ @Test
+ fun verifyPDFViewerInACustomTabTest() {
+ val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+ val pdfFormResource = TestAssetHelper.getPdfFormAsset(mockWebServer)
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ customTabPage.url.toString(),
+ ),
+ )
+
+ customTabScreen {
+ clickPageObject(itemWithText("PDF form file"))
+ clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
+ waitForPageToLoad()
+ verifyPDFReaderToolbarItems()
+ verifyCustomTabCloseButton()
+ verifyCustomTabsSiteInfoButton()
+ verifyCustomTabToolbarTitle("pdfForm.pdf")
+ verifyCustomTabUrl(pdfFormResource.url.toString())
+ verifyCustomTabsShareButton()
+ verifyMainMenuButton()
+ clickCustomTabCloseButton()
+ }
+ homeScreen {
+ verifyHomeScreenAppBarItems()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2239117
+ @Test
+ fun verifyCustomTabETPSheetAndToggleTest() {
+ val customTabPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ intentReceiverActivityTestRule.launchActivity(
+ createCustomTabIntent(
+ pageUrl = customTabPage.url.toString(),
+ customActionButtonDescription = customTabActionButton,
+ ),
+ )
+
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyEnhancedTrackingProtectionSheetStatus(status = "ON", state = true)
+ }.toggleEnhancedTrackingProtectionFromSheet {
+ verifyEnhancedTrackingProtectionSheetStatus(status = "OFF", state = false)
+ }.closeEnhancedTrackingProtectionSheet {
+ }
+
+ openAppFromExternalLink(customTabPage.url.toString())
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ switchEnhancedTrackingProtectionToggle()
+ verifyEnhancedTrackingProtectionOptionsEnabled(enabled = false)
+ }
+
+ exitMenu()
+
+ browserScreen {
+ }.goBack {
+ // Actually exiting to the previously opened custom tab
+ }
+
+ enhancedTrackingProtection {
+ verifyETPSectionIsDisplayedInQuickSettingsSheet(isDisplayed = false)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt
new file mode 100644
index 0000000000..1a3cf129f2
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.DeepLinkRobot
+
+/**
+ * Tests for verifying basic functionality of deep links
+ * - fenix://home
+ * - fenix://open
+ * - fenix://settings_notifications — take the user to the notification settings page
+ * - fenix://settings_privacy — take the user to the privacy settings page.
+ * - fenix://settings_search_engine — take the user to the search engine page, to set the default search engine.
+ * - fenix://home_collections — take the user to the home screen to see the list of collections.
+ * - fenix://urls_history — take the user to the history list.
+ * - fenix://urls_bookmarks — take the user to the bookmarks list
+ * - fenix://settings_logins — take the user to the settings page to do with logins (not the saved logins).
+ **/
+
+@Ignore("All tests perma-failing, see: https://github.com/mozilla-mobile/fenix/issues/13491")
+class DeepLinkTest : TestSetup() {
+ private val robot = DeepLinkRobot()
+
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule()
+
+ @Test
+ fun openHomeScreen() {
+ robot.openHomeScreen {
+ verifyHomeComponent()
+ }
+ robot.openSettings { /* move away from the home screen */ }
+ robot.openHomeScreen {
+ verifyHomeComponent()
+ }
+ }
+
+ @Test
+ fun openURL() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ robot.openURL(genericURL.url.toString()) {
+ verifyUrl(genericURL.url.toString())
+ }
+ }
+
+ @Test
+ fun openBookmarks() {
+ robot.openBookmarks {
+ // verify we can see headings.
+ verifyFolderTitle("Desktop Bookmarks")
+ }
+ }
+
+ @Test
+ fun openHistory() {
+ robot.openHistory {
+ verifyHistoryMenuView()
+ }
+ }
+
+ @Test
+ fun openCollections() {
+ robot.openCollections {
+ verifyCollectionsHeader()
+ }
+ }
+
+ @Test
+ fun openSettings() {
+ robot.openSettings {
+ verifyGeneralHeading()
+ verifyAdvancedHeading()
+ }
+ }
+
+ @Test
+ fun openSettingsLogins() {
+ robot.openSettingsLogins {
+ verifyDefaultView()
+ verifyDefaultValueAutofillLogins(InstrumentationRegistry.getInstrumentation().targetContext)
+ }
+ }
+
+ @Test
+ fun openSettingsPrivacy() {
+ robot.openSettingsPrivacy {
+ verifyPrivacyHeading()
+ }
+ }
+
+ @Test
+ fun openSettingsTrackingProtection() {
+ robot.openSettingsTrackingProtection {
+ verifyEnhancedTrackingProtectionSummary()
+ }
+ }
+
+ @Ignore("Crashing, see: https://github.com/mozilla-mobile/fenix/issues/11239")
+ @Test
+ fun openSettingsSearchEngine() {
+ robot.openSettingsSearchEngine {
+ verifyDefaultSearchEngineHeader()
+ }
+ }
+
+ @Test
+ fun openSettingsNotifications() {
+ robot.openSettingsNotification {
+ verifyNotifications()
+ }
+ }
+
+ @Test
+ fun openMakeDefaultBrowser() {
+ robot.openMakeDefaultBrowser {
+ verifyMakeDefaultBrowser()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt
new file mode 100644
index 0000000000..1ff999f428
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt
@@ -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.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.downloadRobot
+
+/**
+ * Test for verifying downloading a list of different file types:
+ * - Initiates a download
+ * - Verifies download prompt
+ * - Verifies downloading of varying file types and the appearance inside the Downloads listing.
+ **/
+@RunWith(Parameterized::class)
+class DownloadFileTypesTest(fileName: String) : TestSetup() {
+ /* Remote test page managed by Mozilla Mobile QA team at https://github.com/mozilla-mobile/testapp */
+ private val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
+ private var downloadFile: String = fileName
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ companion object {
+ // Creating test data. The test will take each file name as a parameter and run it individually.
+ @JvmStatic
+ @Parameterized.Parameters
+ fun downloadList() = listOf(
+ "smallZip.zip",
+ "MyDocument.docx",
+ "audioSample.mp3",
+ "textfile.txt",
+ "web_icon.png",
+ "videoSample.webm",
+ "CSVfile.csv",
+ "XMLfile.xml",
+ "tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg",
+ )
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251028
+ @SmokeTest
+ @Test
+ fun allFilesAppearInDownloadsMenuTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = downloadFile)
+ verifyDownloadCompleteNotificationPopup()
+ }.closeDownloadPrompt {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ waitForDownloadsListToExist()
+ verifyDownloadedFileName(downloadFile)
+ verifyDownloadedFileIcon()
+ }.exitDownloadsManagerToBrowser { }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt
new file mode 100644
index 0000000000..23c8e2ac3c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt
@@ -0,0 +1,356 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
+import org.mozilla.fenix.helpers.AppAndSystemHelper.deleteDownloadedFileOnStorage
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
+import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
+import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_DOCS
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.downloadRobot
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.notificationShade
+
+/**
+ * Tests for verifying basic functionality of download
+ *
+ * - Initiates a download
+ * - Verifies download prompt
+ * - Verifies download notification and actions
+ * - Verifies managing downloads inside the Downloads listing.
+ **/
+class DownloadTest : TestSetup() {
+ /* Remote test page managed by Mozilla Mobile QA team at https://github.com/mozilla-mobile/testapp */
+ private val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
+ private var downloadFile: String = ""
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243844
+ @Test
+ fun verifyTheDownloadPromptsTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "web_icon.png")
+ verifyDownloadCompleteNotificationPopup()
+ }.clickOpen("image/png") {}
+ downloadRobot {
+ verifyPhotosAppOpens()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299405
+ @Test
+ fun verifyTheDownloadFailedNotificationsTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip")
+ setNetworkEnabled(enabled = false)
+ verifyDownloadFailedPrompt("1GB.zip")
+ setNetworkEnabled(enabled = true)
+ clickTryAgainButton()
+ }
+ mDevice.openNotification()
+ notificationShade {
+ verifySystemNotificationDoesNotExist("Download failed")
+ verifySystemNotificationExists("1GB.zip")
+ }.closeNotificationTray {}
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2298616
+ @Test
+ fun verifyDownloadCompleteNotificationTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "web_icon.png")
+ verifyDownloadCompleteNotificationPopup()
+ }
+ mDevice.openNotification()
+ notificationShade {
+ verifySystemNotificationExists("Download completed")
+ clickNotification("Download completed")
+ assertExternalAppOpens(GOOGLE_APPS_PHOTOS)
+ mDevice.pressBack()
+ mDevice.openNotification()
+ verifySystemNotificationExists("Download completed")
+ swipeDownloadNotification(
+ direction = "Left",
+ shouldDismissNotification = true,
+ canExpandNotification = false,
+ )
+ verifySystemNotificationDoesNotExist("Firefox Fenix")
+ }.closeNotificationTray {}
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/451563
+ @Ignore("Failing: Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1813521")
+ @SmokeTest
+ @Test
+ fun pauseResumeCancelDownloadTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
+ }
+ mDevice.openNotification()
+ notificationShade {
+ verifySystemNotificationExists("Firefox Fenix")
+ expandNotificationMessage()
+ clickDownloadNotificationControlButton("PAUSE")
+ verifySystemNotificationExists("Download paused")
+ clickDownloadNotificationControlButton("RESUME")
+ clickDownloadNotificationControlButton("CANCEL")
+ verifySystemNotificationDoesNotExist("3GB.zip")
+ mDevice.pressBack()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyEmptyDownloadsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301474
+ @Test
+ fun openDownloadedFileFromDownloadsMenuTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "web_icon.png")
+ verifyDownloadCompleteNotificationPopup()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyDownloadedFileName("web_icon.png")
+ openDownloadedFile("web_icon.png")
+ verifyPhotosAppOpens()
+ mDevice.pressBack()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1114970
+ @Test
+ fun deleteDownloadedFileTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip")
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyDownloadedFileName("smallZip.zip")
+ deleteDownloadedItem("smallZip.zip")
+ clickSnackbarButton("UNDO")
+ verifyDownloadedFileName("smallZip.zip")
+ deleteDownloadedItem("smallZip.zip")
+ verifyEmptyDownloadsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2302662
+ @Test
+ fun deleteMultipleDownloadedFilesTest() {
+ val firstDownloadedFile = "smallZip.zip"
+ val secondDownloadedFile = "textfile.txt"
+
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = firstDownloadedFile)
+ verifyDownloadedFileName(firstDownloadedFile)
+ }.closeDownloadPrompt {
+ }.clickDownloadLink(secondDownloadedFile) {
+ verifyDownloadPrompt(secondDownloadedFile)
+ }.clickDownload {
+ verifyDownloadedFileName(secondDownloadedFile)
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyDownloadedFileName(firstDownloadedFile)
+ verifyDownloadedFileName(secondDownloadedFile)
+ longClickDownloadedItem(firstDownloadedFile)
+ selectDownloadedItem(secondDownloadedFile)
+ openMultiSelectMoreOptionsMenu()
+ clickMultiSelectRemoveButton()
+ clickSnackbarButton("UNDO")
+ verifyDownloadedFileName(firstDownloadedFile)
+ verifyDownloadedFileName(secondDownloadedFile)
+ longClickDownloadedItem(firstDownloadedFile)
+ selectDownloadedItem(secondDownloadedFile)
+ openMultiSelectMoreOptionsMenu()
+ clickMultiSelectRemoveButton()
+ verifyEmptyDownloadsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301537
+ @Test
+ fun fileDeletedFromStorageIsDeletedEverywhereTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip")
+ verifyDownloadCompleteNotificationPopup()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ waitForDownloadsListToExist()
+ verifyDownloadedFileName("smallZip.zip")
+ deleteDownloadedFileOnStorage("smallZip.zip")
+ }.exitDownloadsManagerToBrowser {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyEmptyDownloadsList()
+ exitMenu()
+ }
+
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip")
+ verifyDownloadCompleteNotificationPopup()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ waitForDownloadsListToExist()
+ verifyDownloadedFileName("smallZip.zip")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/457112
+ @Test
+ fun systemNotificationCantBeDismissedWhileInProgressTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
+ }
+ browserScreen {
+ }.openNotificationShade {
+ verifySystemNotificationExists("Firefox Fenix")
+ expandNotificationMessage()
+ swipeDownloadNotification(direction = "Left", shouldDismissNotification = false)
+ clickDownloadNotificationControlButton("PAUSE")
+ swipeDownloadNotification(direction = "Right", shouldDismissNotification = false)
+ clickDownloadNotificationControlButton("CANCEL")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299297
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1842154")
+ @Test
+ fun notificationCanBeDismissedIfDownloadIsInterruptedTest() {
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip")
+ }
+
+ setNetworkEnabled(enabled = false)
+
+ browserScreen {
+ }.openNotificationShade {
+ expandNotificationMessage()
+ verifySystemNotificationExists("Download failed")
+ swipeDownloadNotification("Left", true)
+ verifySystemNotificationDoesNotExist("Firefox Fenix")
+ }.closeNotificationTray {}
+
+ downloadRobot {
+ }.closeDownloadPrompt {
+ verifyDownloadPromptIsDismissed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1632384
+ @Test
+ fun warningWhenClosingPrivateTabsWhileDownloadingTest() {
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
+ }
+ browserScreen {
+ }.openTabDrawer {
+ closeTab()
+ }
+ browserScreen {
+ verifyCancelPrivateDownloadsPrompt("1")
+ clickStayInPrivateBrowsingPromptButton()
+ }.openNotificationShade {
+ verifySystemNotificationExists("Firefox Fenix")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2302663
+ @Test
+ fun cancelActivePrivateBrowsingDownloadsTest() {
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "3GB.zip")
+ }
+ browserScreen {
+ }.openTabDrawer {
+ closeTab()
+ }
+ browserScreen {
+ verifyCancelPrivateDownloadsPrompt("1")
+ clickCancelPrivateDownloadsPromptButton()
+ }.openNotificationShade {
+ verifySystemNotificationDoesNotExist("Firefox Fenix")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2048448
+ // Save edited PDF file from the share overlay
+ @SmokeTest
+ @Test
+ fun saveAsPdfFunctionalityTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+ downloadFile = "pdfForm.pdf"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ waitForPageToLoad()
+ fillPdfForm("Firefox")
+ }.openThreeDotMenu {
+ }.clickShareButton {
+ }.clickSaveAsPDF {
+ verifyDownloadPrompt("pdfForm.pdf")
+ }.clickDownload {
+ }.clickOpen("application/pdf") {
+ assertExternalAppOpens(GOOGLE_DOCS)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244125
+ @Test
+ fun restartDownloadFromAppNotificationAfterConnectionIsInterruptedTest() {
+ downloadFile = "3GB.zip"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
+ waitForPageToLoad()
+ }.clickDownloadLink(downloadFile) {
+ verifyDownloadPrompt(downloadFile)
+ setNetworkEnabled(false)
+ }.clickDownload {
+ verifyDownloadFailedPrompt(downloadFile)
+ setNetworkEnabled(true)
+ clickTryAgainButton()
+ }
+ browserScreen {
+ }.openNotificationShade {
+ verifySystemNotificationExists("Firefox Fenix")
+ expandNotificationMessage()
+ clickDownloadNotificationControlButton("CANCEL")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/EnhancedTrackingProtectionTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/EnhancedTrackingProtectionTest.kt
new file mode 100644
index 0000000000..01e627bc0e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/EnhancedTrackingProtectionTest.kt
@@ -0,0 +1,522 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import androidx.test.espresso.Espresso.pressBack
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.getEnhancedTrackingProtectionAsset
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.enhancedTrackingProtection
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic UI functionality of Enhanced Tracking Protection
+ *
+ * Including
+ * - Verifying default states
+ * - Verifying Enhanced Tracking Protection notification bubble
+ * - Verifying Enhanced Tracking Protection content sheet
+ * - Verifying Enhanced Tracking Protection content sheet details
+ * - Verifying Enhanced Tracking Protection toggle
+ * - Verifying Enhanced Tracking Protection options and functionality
+ * - Verifying Enhanced Tracking Protection site exceptions
+ */
+
+class EnhancedTrackingProtectionTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule(
+ isJumpBackInCFREnabled = false,
+ isTCPCFREnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ )
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416046
+ @Test
+ fun testETPSettingsItemsAndSubMenus() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyEnhancedTrackingProtectionButton()
+ verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard")
+ }.openEnhancedTrackingProtectionSubMenu {
+ verifyEnhancedTrackingProtectionSummary()
+ verifyLearnMoreText()
+ verifyEnhancedTrackingProtectionTextWithSwitchWidget()
+ verifyTrackingProtectionSwitchEnabled()
+ verifyEnhancedTrackingProtectionOptionsEnabled()
+ verifyEnhancedTrackingProtectionLevelSelected("Standard (default)", true)
+ verifyStandardOptionDescription()
+ verifyStrictOptionDescription()
+ verifyGPCTextWithSwitchWidget()
+ verifyGPCSwitchEnabled(false)
+ selectTrackingProtectionOption("Custom")
+ verifyCustomTrackingProtectionSettings()
+ scrollToElementByText("Standard (default)")
+ verifyWhatsBlockedByStandardETPInfo()
+ pressBack()
+ verifyWhatsBlockedByStrictETPInfo()
+ pressBack()
+ verifyWhatsBlockedByCustomETPInfo()
+ pressBack()
+ }.openExceptions {
+ verifyTPExceptionsDefaultView()
+ openExceptionsLearnMoreLink()
+ }
+ browserScreen {
+ verifyUrl("support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-android")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1514599
+ @Test
+ fun verifyETPStateIsReflectedInTPSheetTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ switchEnhancedTrackingProtectionToggle()
+ verifyEnhancedTrackingProtectionOptionsEnabled(false)
+ }.goBack {
+ verifySettingsOptionSummary("Enhanced Tracking Protection", "Off")
+ exitMenu()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) { }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyETPSwitchVisibility(false)
+ }.closeEnhancedTrackingProtectionSheet {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ switchEnhancedTrackingProtectionToggle()
+ verifyEnhancedTrackingProtectionOptionsEnabled(true)
+ }.goBack {
+ }.goBackToBrowser { }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyETPSwitchVisibility(true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339712
+ // Tests adding ETP exceptions to websites and keeping that preference after restart
+ @SmokeTest
+ @Test
+ fun disablingETPOnAWebsiteAddsItToExceptionListTest() {
+ val firstPage = getGenericAsset(mockWebServer, 1)
+ val secondPage = "example.com"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstPage.url) {}
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.toggleEnhancedTrackingProtectionFromSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
+ }.closeEnhancedTrackingProtectionSheet {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondPage.toUri()) {
+ verifyPageContent("Example Domain")
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("ON", true)
+ }.toggleEnhancedTrackingProtectionFromSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
+ }
+ restartApp(activityTestRule)
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339714
+ @Test
+ fun enablingETPOnAWebsiteRemovesItFromTheExceptionListTest() {
+ val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingPage.url) {
+ waitForPageToLoad()
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.toggleEnhancedTrackingProtectionFromSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
+ }.closeEnhancedTrackingProtectionSheet {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ }.openExceptions {
+ verifySiteExceptionExists(trackingPage.url.host.toString(), true)
+ exitMenu()
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.toggleEnhancedTrackingProtectionFromSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("ON", true)
+ }.openProtectionSettings {
+ }.openExceptions {
+ verifySiteExceptionExists(trackingPage.url.host.toString(), false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339713
+ // Tests removing TP exceptions individually or all at once
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1865781")
+ @Test
+ fun clearWebsitesFromTPExceptionListTest() {
+ val firstPage = getGenericAsset(mockWebServer, 1)
+ val secondPage = "example.com"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstPage.url) {}
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.toggleEnhancedTrackingProtectionFromSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
+ }.closeEnhancedTrackingProtectionSheet {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondPage.toUri()) {
+ verifyPageContent("Example Domain")
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.toggleEnhancedTrackingProtectionFromSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
+ }.closeEnhancedTrackingProtectionSheet {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ }.openExceptions {
+ removeOneSiteException(secondPage)
+ }.disableExceptions {
+ verifyTPExceptionsDefaultView()
+ exitMenu()
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("ON", true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/417444
+ @Test
+ fun verifyTrackersBlockedWithStandardTPTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+ val trackingProtectionTest = getEnhancedTrackingProtectionAsset(mockWebServer).url
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyEnhancedTrackingProtectionButton()
+ verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard")
+ exitMenu()
+ }
+
+ // browsing a generic page to allow GV to load on a fresh run
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ verifyPageContent(genericPage.content)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingProtectionTest) {
+ verifyTrackingProtectionWebContent("social not blocked")
+ verifyTrackingProtectionWebContent("ads not blocked")
+ verifyTrackingProtectionWebContent("analytics not blocked")
+ verifyTrackingProtectionWebContent("Fingerprinting blocked")
+ verifyTrackingProtectionWebContent("Cryptomining blocked")
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("ON", true)
+ }.openDetails {
+ verifyCrossSiteCookiesBlocked(true)
+ navigateBackToDetails()
+ verifyCryptominersBlocked(true)
+ navigateBackToDetails()
+ verifyFingerprintersBlocked(true)
+ navigateBackToDetails()
+ verifyTrackingContentBlocked(false)
+ }.closeEnhancedTrackingProtectionSheet {}
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/417441
+ @Test
+ fun verifyTrackersBlockedWithStrictTPTest() {
+ appContext.settings().setStrictETP()
+ val genericPage = getGenericAsset(mockWebServer, 1)
+ val trackingProtectionTest = getEnhancedTrackingProtectionAsset(mockWebServer).url
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyEnhancedTrackingProtectionButton()
+ verifySettingsOptionSummary("Enhanced Tracking Protection", "Strict")
+ exitMenu()
+ }
+
+ // browsing a generic page to allow GV to load on a fresh run
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingProtectionTest) {
+ verifyTrackingProtectionWebContent("social blocked")
+ verifyTrackingProtectionWebContent("ads blocked")
+ verifyTrackingProtectionWebContent("analytics blocked")
+ verifyTrackingProtectionWebContent("Fingerprinting blocked")
+ verifyTrackingProtectionWebContent("Cryptomining blocked")
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ verifyEnhancedTrackingProtectionSheetStatus("ON", true)
+ }.openDetails {
+ verifySocialMediaTrackersBlocked(true)
+ navigateBackToDetails()
+ verifyCryptominersBlocked(true)
+ navigateBackToDetails()
+ verifyFingerprintersBlocked(true)
+ navigateBackToDetails()
+ verifyTrackingContentBlocked(true)
+ viewTrackingContentBlockList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561637
+ @SmokeTest
+ @Test
+ fun verifyTrackersBlockedWithCustomTPTest() {
+ val genericWebPage = getGenericAsset(mockWebServer, 1)
+ val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ selectTrackingProtectionOption("Custom")
+ verifyCustomTrackingProtectionSettings()
+ }.goBack {
+ verifySettingsOptionSummary("Enhanced Tracking Protection", "Custom")
+ exitMenu()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericWebPage.url) {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingPage.url) {
+ verifyTrackingProtectionWebContent("social blocked")
+ verifyTrackingProtectionWebContent("ads blocked")
+ verifyTrackingProtectionWebContent("analytics blocked")
+ verifyTrackingProtectionWebContent("Fingerprinting blocked")
+ verifyTrackingProtectionWebContent("Cryptomining blocked")
+ }
+
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.openDetails {
+ verifyCryptominersBlocked(true)
+ navigateBackToDetails()
+ verifyFingerprintersBlocked(true)
+ navigateBackToDetails()
+ verifyTrackingContentBlocked(true)
+ viewTrackingContentBlockList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/562710
+ // Tests the trackers blocked with the following Custom TP set up:
+ // - Cookies set to "All cookies"
+ // - Tracking content option OFF
+ // - Fingerprinters, cryptominers and redirect trackers checked
+ @Test
+ fun customizedTrackingProtectionOptionsTest() {
+ val genericWebPage = getGenericAsset(mockWebServer, 1)
+ val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ selectTrackingProtectionOption("Custom")
+ verifyCustomTrackingProtectionSettings()
+ selectTrackingProtectionOption("Isolate cross-site cookies")
+ selectTrackingProtectionOption("All cookies (will cause websites to break)")
+ selectTrackingProtectionOption("Tracking content")
+ }.goBackToHomeScreen {
+ mDevice.waitForIdle()
+ }.openNavigationToolbar {
+ // browsing a basic page to allow GV to load on a fresh run
+ }.enterURLAndEnterToBrowser(genericWebPage.url) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingPage.url) {
+ verifyTrackingProtectionWebContent("social not blocked")
+ verifyTrackingProtectionWebContent("ads not blocked")
+ verifyTrackingProtectionWebContent("analytics not blocked")
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.openDetails {
+ verifyCrossSiteCookiesBlocked(true)
+ navigateBackToDetails()
+ verifyCryptominersBlocked(true)
+ navigateBackToDetails()
+ verifyFingerprintersBlocked(true)
+ navigateBackToDetails()
+ verifyTrackingContentBlocked(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/562709
+ @Test
+ fun verifyTrackersBlockedWithCustomTPOptionsDisabledTest() {
+ val genericWebPage = getGenericAsset(mockWebServer, 1)
+ val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ selectTrackingProtectionOption("Custom")
+ verifyCustomTrackingProtectionSettings()
+ selectTrackingProtectionOption("Cookies")
+ selectTrackingProtectionOption("Tracking content")
+ selectTrackingProtectionOption("Cryptominers")
+ selectTrackingProtectionOption("Fingerprinters")
+ selectTrackingProtectionOption("Redirect Trackers")
+ }.goBackToHomeScreen {
+ mDevice.waitForIdle()
+ }.openNavigationToolbar {
+ // browsing a basic page to allow GV to load on a fresh run
+ }.enterURLAndEnterToBrowser(genericWebPage.url) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingPage.url) {
+ verifyTrackingProtectionWebContent("social not blocked")
+ verifyTrackingProtectionWebContent("ads not blocked")
+ verifyTrackingProtectionWebContent("analytics not blocked")
+ verifyTrackingProtectionWebContent("Fingerprinting not blocked")
+ verifyTrackingProtectionWebContent("Cryptomining not blocked")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2106997
+ @Test
+ fun verifyTrackingContentBlockedOnlyInPrivateTabsTest() {
+ val genericWebPage = getGenericAsset(mockWebServer, 1)
+ val trackingPage = getEnhancedTrackingProtectionAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ verifyEnhancedTrackingProtectionOptionsEnabled()
+ selectTrackingProtectionOption("Custom")
+ verifyCustomTrackingProtectionSettings()
+ selectTrackingProtectionOption("In all tabs")
+ selectTrackingProtectionOption("Only in Private tabs")
+ }.goBackToHomeScreen {
+ }.openNavigationToolbar {
+ // browsing a basic page to allow GV to load on a fresh run
+ }.enterURLAndEnterToBrowser(genericWebPage.url) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingPage.url) {
+ verifyTrackingProtectionWebContent("social not blocked")
+ verifyTrackingProtectionWebContent("ads not blocked")
+ verifyTrackingProtectionWebContent("analytics not blocked")
+ verifyTrackingProtectionWebContent("Fingerprinting blocked")
+ verifyTrackingProtectionWebContent("Cryptomining blocked")
+ }.goToHomescreen {
+ }.togglePrivateBrowsingMode()
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingPage.url) {
+ verifyTrackingProtectionWebContent("social blocked")
+ verifyTrackingProtectionWebContent("ads blocked")
+ verifyTrackingProtectionWebContent("analytics blocked")
+ verifyTrackingProtectionWebContent("Fingerprinting blocked")
+ verifyTrackingProtectionWebContent("Cryptomining blocked")
+ }
+ enhancedTrackingProtection {
+ }.openEnhancedTrackingProtectionSheet {
+ }.openDetails {
+ verifyCrossSiteCookiesBlocked(true)
+ navigateBackToDetails()
+ verifyCryptominersBlocked(true)
+ navigateBackToDetails()
+ verifyFingerprintersBlocked(true)
+ navigateBackToDetails()
+ verifyTrackingContentBlocked(true)
+ viewTrackingContentBlockList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2285368
+ @SmokeTest
+ @Test
+ fun blockCookiesStorageAccessTest() {
+ // With Standard TrackingProtection settings
+ val genericWebPage = getGenericAsset(mockWebServer, 1)
+ val testPage = mockWebServer.url("pages/cross-site-cookies.html").toString().toUri()
+ val originSite = "https://mozilla-mobile.github.io"
+ val currentSite = "http://localhost:${mockWebServer.port}"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericWebPage.url) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage) {
+ waitForPageToLoad()
+ }.clickRequestStorageAccessButton {
+ verifyCrossOriginCookiesPermissionPrompt(originSite, currentSite)
+ }.clickPagePermissionButton(allow = false) {
+ verifyPageContent("access denied")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2285369
+ @SmokeTest
+ @Test
+ fun allowCookiesStorageAccessTest() {
+ // With Standard TrackingProtection settings
+ val genericWebPage = getGenericAsset(mockWebServer, 1)
+ val testPage = mockWebServer.url("pages/cross-site-cookies.html").toString().toUri()
+ val originSite = "https://mozilla-mobile.github.io"
+ val currentSite = "http://localhost:${mockWebServer.port}"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericWebPage.url) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage) {
+ waitForPageToLoad()
+ }.clickRequestStorageAccessButton {
+ verifyCrossOriginCookiesPermissionPrompt(originSite, currentSite)
+ }.clickPagePermissionButton(allow = true) {
+ verifyPageContent("access granted")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/FirefoxSuggestTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/FirefoxSuggestTest.kt
new file mode 100644
index 0000000000..2e881e16fe
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/FirefoxSuggestTest.kt
@@ -0,0 +1,267 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
+import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredFxSuggestPlaceHolder
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the Firefox suggest search fragment
+ *
+ */
+
+class FirefoxSuggestTest : TestSetup() {
+
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityTestRule(
+ skipOnboarding = true,
+ isPocketEnabled = false,
+ isJumpBackInCFREnabled = false,
+ isRecentTabsFeatureEnabled = false,
+ isTCPCFREnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ tabsTrayRewriteEnabled = false,
+ ),
+ ) { it.activity }
+
+ private val sponsoredKeyWords: Map> =
+ mapOf(
+ "Amazon" to
+ listOf(
+ "Amazon.com - Official Site",
+ "amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
+ ),
+ "Nike" to
+ listOf(
+ "Nike.com - Official Site",
+ "nike.com/?cp=16423867261_search_318370984us128${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
+ ),
+ "Houzz" to listOf(
+ "Houzz.com - Official Site",
+ "houzz.com/products?m_refid=us-dsp-mpl-admp-219577_15416306_kwd-353208810&adcid=319104989&mfadid=adm&utm_source=admarketplace&utm_medium=cpc&utm_campaign=Privacy&utm_term=houzz&utm_content=319104989us1287${getSponsoredFxSuggestPlaceHolder()}",
+ ),
+ "Spanx" to listOf(
+ "SPANX® - Official Site",
+ "spanx.com/?utm_source=admarketplace&utm_medium=cpc&utm_campaign=privacy&utm_content=319093361us1202${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
+ ),
+ "Bloom" to listOf(
+ "Bloomingdales.com - Official Site",
+ "bloomingdales.com/?cm_mmc=Admarketplace-_-Privacy-_-Privacy-_-privacy%20instant%20suggest-_-319093353us1228${getSponsoredFxSuggestPlaceHolder()}-_-kclickid__kenshoo_clickid_&mfadid=adm",
+ ),
+ "Groupon" to listOf(
+ "groupon.com - Discover & Save!",
+ "groupon.com/?utm_source=google&utm_medium=cpc&utm_campaign=us_dt_sea_ggl_txt_smp_sr_cbp_ch1_nbr_k*groupon_m*broad_d*ADMRKT_319093357us1279${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
+ ),
+ )
+
+ private val sponsoredKeyWord = sponsoredKeyWords.keys.random()
+
+ private val nonSponsoredKeyWords: Map> =
+ mapOf(
+ "Marvel" to
+ listOf(
+ "Wikipedia - Marvel Cinematic Universe",
+ "wikipedia.org/wiki/Marvel_Cinematic_Universe",
+ ),
+ "Apple" to
+ listOf(
+ "Wikipedia - Apple Inc.",
+ "wikipedia.org/wiki/Apple_Inc",
+ ),
+ "Africa" to listOf(
+ "Wikipedia - African Union",
+ "wikipedia.org/wiki/African_Union",
+ ),
+ "Ultimate" to listOf(
+ "Wikipedia - Ultimate Fighting Championship",
+ "wikipedia.org/wiki/Ultimate_Fighting_Championship",
+ ),
+ "Youtube" to listOf(
+ "Wikipedia - YouTube",
+ "wikipedia.org/wiki/YouTube",
+ ),
+ "Fifa" to listOf(
+ "Wikipedia - FIFA World Cup",
+ "en.m.wikipedia.org/wiki/FIFA_World_Cup",
+ ),
+ )
+
+ private val nonSponsoredKeyWord = nonSponsoredKeyWords.keys.random()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348361
+ // Known bug that might affect this UI test: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ @SmokeTest
+ @Test
+ fun verifyFirefoxSuggestSponsoredSearchResultsTest() {
+ runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
+ navigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = sponsoredKeyWord)
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
+ "Sponsored",
+ ),
+ searchTerm = sponsoredKeyWord,
+ )
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348362
+ // Known bug that might affect this UI test: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ @Test
+ fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() {
+ runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
+ navigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = sponsoredKeyWord.dropLast(1))
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
+ "Sponsored",
+ ),
+ searchTerm = sponsoredKeyWord.dropLast(1),
+ )
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348363
+ // Known bug that might affect this UI test: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ @Test
+ fun openFirefoxSuggestSponsoredSearchResultsTest() {
+ runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
+ navigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = sponsoredKeyWord)
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
+ "Sponsored",
+ ),
+ searchTerm = sponsoredKeyWord,
+ )
+ }.clickSearchSuggestion(sponsoredKeyWords.getValue(sponsoredKeyWord)[0]) {
+ verifyUrl(sponsoredKeyWords.getValue(sponsoredKeyWord)[1])
+ verifyTabCounter("1")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348369
+ // Known bug that might affect this UI test: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ @Test
+ fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() {
+ runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
+ navigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = sponsoredKeyWord)
+ deleteSearchKeywordCharacters(numberOfDeletionSteps = 1)
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
+ "Sponsored",
+ ),
+ searchTerm = sponsoredKeyWord,
+ shouldEditKeyword = true,
+ numberOfDeletionSteps = 1,
+ )
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348374
+ // Known bug that might affect this UI test: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1882035")
+ @SmokeTest
+ @Test
+ fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() {
+ runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
+ navigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = nonSponsoredKeyWord)
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
+ ),
+ searchTerm = nonSponsoredKeyWord,
+ )
+ verifySuggestionsAreNotDisplayed(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Sponsored",
+ ),
+ )
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375
+ // Known bug that might affect this UI test: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1882035")
+ @Test
+ fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() {
+ runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
+ navigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = nonSponsoredKeyWord.dropLast(1))
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
+ ),
+ searchTerm = nonSponsoredKeyWord.dropLast(1),
+ )
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348376
+ // Known bug that might affect this UI test: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1882035")
+ @Test
+ fun openFirefoxSuggestNonSponsoredSearchResultsTest() {
+ runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
+ navigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = nonSponsoredKeyWord)
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
+ ),
+ searchTerm = nonSponsoredKeyWord,
+ )
+ }.clickSearchSuggestion(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0]) {
+ waitForPageToLoad()
+ verifyUrl(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[1])
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/GlobalPrivacyControlTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/GlobalPrivacyControlTest.kt
new file mode 100644
index 0000000000..02e4cc2242
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/GlobalPrivacyControlTest.kt
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.TestAsset
+import org.mozilla.fenix.helpers.TestAssetHelper.getGPCTestAsset
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for Global Privacy Control setting.
+ */
+
+class GlobalPrivacyControlTest : TestSetup() {
+ private lateinit var gpcPage: TestAsset
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule(
+ isJumpBackInCFREnabled = false,
+ isTCPCFREnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ skipOnboarding = true,
+ )
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ gpcPage = getGPCTestAsset(mockWebServer)
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2429327
+ @Test
+ fun testGPCinNormalBrowsing() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(gpcPage.url) {
+ verifyPageContent("GPC not enabled.")
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ scrollToGCPSettings()
+ verifyGPCTextWithSwitchWidget()
+ verifyGPCSwitchEnabled(false)
+ switchGPCToggle()
+ }.goBack {
+ }.goBackToBrowser {
+ verifyPageContent("GPC is enabled.")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2429364
+ @Test
+ fun testGPCinPrivateBrowsing() {
+ homeScreen { }.togglePrivateBrowsingMode()
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(gpcPage.url) {
+ verifyPageContent("GPC is enabled.")
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openEnhancedTrackingProtectionSubMenu {
+ scrollToGCPSettings()
+ verifyGPCTextWithSwitchWidget()
+ verifyGPCSwitchEnabled(false)
+ switchGPCToggle()
+ }.goBack {
+ }.goBackToBrowser {
+ verifyPageContent("GPC is enabled.")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt
new file mode 100644
index 0000000000..28df761956
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt
@@ -0,0 +1,450 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import androidx.test.espresso.Espresso.pressBack
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MockBrowserDataHelper
+import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.historyMenu
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic functionality of history
+ *
+ */
+class HistoryTest : TestSetup() {
+ @get:Rule
+ val activityTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule(isJumpBackInCFREnabled = false),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243285
+ @Test
+ fun verifyEmptyHistoryMenuTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ verifyHistoryButton()
+ }.openHistory {
+ verifyHistoryMenuView()
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2302742
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ @SmokeTest
+ @Test
+ fun verifyHistoryMenuWithHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ verifyHistoryMenuView()
+ verifyVisitedTimeTitle()
+ verifyFirstTestPageTitle("Test_Page_1")
+ verifyTestPageUrl(firstWebPage.url)
+ verifyDeleteHistoryItemButton("Test_Page_1")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243288
+ @Test
+ fun deleteHistoryItemTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ clickDeleteHistoryButton(firstWebPage.url.toString())
+ }
+ verifyUndoDeleteSnackBarButton()
+ clickSnackbarButton("UNDO")
+ verifyHistoryItemExists(true, firstWebPage.url.toString())
+ clickDeleteHistoryButton(firstWebPage.url.toString())
+ verifySnackBarText(expectedText = "Deleted")
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1848881
+ @SmokeTest
+ @Test
+ fun deleteAllHistoryTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ clickDeleteAllHistoryButton()
+ }
+ verifyDeleteConfirmationMessage()
+ selectEverythingOption()
+ cancelDeleteHistory()
+ verifyHistoryItemExists(true, firstWebPage.url.toString())
+ clickDeleteAllHistoryButton()
+ verifyDeleteConfirmationMessage()
+ selectEverythingOption()
+ confirmDeleteAllHistory()
+ verifySnackBarText(expectedText = "Browsing data deleted")
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339690
+ @Test
+ fun historyMultiSelectionToolbarItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ verifyMultiSelectionCheckmark()
+ verifyMultiSelectionCounter()
+ verifyShareHistoryButton()
+ verifyCloseToolbarButton()
+ }.closeToolbarReturnToHistory {
+ verifyHistoryMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339696
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
+ @Test
+ fun openMultipleSelectedHistoryItemsInANewTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ homeScreen { }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenNewTab {
+ verifyExistingTabList()
+ verifyNormalModeSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/346098
+ @Test
+ fun openMultipleSelectedHistoryItemsInPrivateTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenPrivateTab {
+ verifyPrivateModeSelected()
+ verifyExistingTabList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/346099
+ @Test
+ fun deleteMultipleSelectedHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ mDevice.waitForIdle()
+ verifyUrl(secondWebPage.url.toString())
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 2),
+ ) {
+ verifyHistoryItemExists(true, firstWebPage.url.toString())
+ verifyHistoryItemExists(true, secondWebPage.url.toString())
+ longTapSelectItem(firstWebPage.url)
+ longTapSelectItem(secondWebPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+ }
+
+ multipleSelectionToolbar {
+ clickMultiSelectionDelete()
+ }
+
+ historyMenu {
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339701
+ @Test
+ fun shareMultipleSelectedHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
+ ) {
+ longTapSelectItem(firstWebPage.url)
+ }
+ }
+
+ multipleSelectionToolbar {
+ clickShareHistoryButton()
+ verifyShareOverlay()
+ verifyShareTabFavicon()
+ verifyShareTabTitle()
+ verifyShareTabUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715627
+ @Test
+ fun verifySearchHistoryViewTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ verifySearchView()
+ verifySearchToolbar(true)
+ verifySearchSelectorButton()
+ verifySearchEngineIcon("history")
+ verifySearchBarPlaceholder("Search history")
+ verifySearchBarPosition(true)
+ tapOutsideToDismissSearchBar()
+ verifySearchToolbar(false)
+ exitMenu()
+ }
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ clickTopToolbarToggle()
+ }
+
+ exitMenu()
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ verifySearchView()
+ verifySearchToolbar(true)
+ verifySearchBarPosition(false)
+ pressBack()
+ }
+ historyMenu {
+ verifyHistoryMenuView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715631
+ @Test
+ fun verifyVoiceSearchInHistoryTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ verifySearchToolbar(true)
+ verifySearchEngineIcon("history")
+ startVoiceSearch()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715632
+ @Test
+ fun verifySearchForHistoryItemsTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
+
+ MockBrowserDataHelper.createHistoryItem(firstWebPage.url.toString())
+ MockBrowserDataHelper.createHistoryItem(secondWebPage.url.toString())
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString(), searchTerm = "generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ }.dismissSearchBar {}
+ historyMenu {
+ }.clickSearchButton {
+ // Search for invalid term
+ typeSearch("Android")
+ verifySuggestionsAreNotDisplayed(
+ activityTestRule,
+ firstWebPage.url.toString(),
+ secondWebPage.url.toString(),
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715634
+ @Test
+ fun verifyDeletedHistoryItemsCanNotBeSearchedTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ verifyPageContent(firstWebPage.content)
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(thirdWebPage.url) {
+ verifyPageContent(thirdWebPage.content)
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryListExists()
+ clickDeleteHistoryButton(firstWebPage.title)
+ verifyHistoryItemExists(false, firstWebPage.title)
+ clickDeleteHistoryButton(secondWebPage.title)
+ verifyHistoryItemExists(false, secondWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, firstWebPage.url.toString())
+ verifySuggestionsAreNotDisplayed(activityTestRule, secondWebPage.url.toString())
+ verifySearchEngineSuggestionResults(
+ activityTestRule,
+ thirdWebPage.url.toString(),
+ searchTerm = "generic",
+ )
+ pressBack()
+ }
+ historyMenu {
+ clickDeleteHistoryButton(thirdWebPage.title)
+ verifyHistoryItemExists(false, firstWebPage.title)
+ }.clickSearchButton {
+ // Search for a valid term
+ typeSearch("generic")
+ verifySuggestionsAreNotDisplayed(activityTestRule, thirdWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903590
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ @SmokeTest
+ @Test
+ fun noHistoryInPrivateBrowsingTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(website.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243287
+ @Test
+ fun openHistoryItemTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openWebsite(defaultWebPage.url) {
+ verifyUrl(defaultWebPage.url.toString())
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt
new file mode 100644
index 0000000000..f1cff8c781
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.searchScreen
+
+/**
+ * Tests for verifying the presence of home screen and first-run homescreen elements
+ *
+ * Note: For private browsing, navigation bar and tabs see separate test class
+ *
+ */
+
+class HomeScreenTest : TestSetup() {
+ @get:Rule(order = 0)
+ val activityTestRule =
+ AndroidComposeTestRule(HomeActivityTestRule.withDefaultSettingsOverrides()) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235396
+ @Test
+ fun homeScreenItemsTest() {
+ // Workaround to make sure the Pocket articles are populated before starting the test.
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.goBack {
+ verifyHomeWordmark()
+ verifyHomePrivateBrowsingButton()
+ verifyExistingTopSitesTabs("Wikipedia")
+ verifyExistingTopSitesTabs("Top Articles")
+ verifyExistingTopSitesTabs("Google")
+ verifyCollectionsHeader()
+ verifyNoCollectionsText()
+ scrollToPocketProvokingStories()
+ verifyThoughtProvokingStories(true)
+ verifyStoriesByTopicItems()
+ verifyCustomizeHomepageButton(true)
+ verifyNavigationToolbar()
+ verifyHomeMenuButton()
+ verifyTabButton()
+ verifyTabCounter("0")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244199
+ @Test
+ fun privateBrowsingHomeScreenItemsTest() {
+ homeScreen { }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ verifyPrivateBrowsingHomeScreenItems()
+ }.openCommonMythsLink {
+ verifyUrl("common-myths-about-private-browsing")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1364362
+ @SmokeTest
+ @Test
+ fun verifyJumpBackInSectionTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isRecentlyVisitedFeatureEnabled = false
+ it.isPocketEnabled = false
+ }
+
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ verifyPageContent(firstWebPage.content)
+ verifyUrl(firstWebPage.url.toString())
+ }.goToHomescreen {
+ verifyJumpBackInSectionIsDisplayed()
+ verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
+ verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
+ verifyJumpBackInShowAllButton()
+ }.clickJumpBackInShowAllButton {
+ verifyExistingOpenTabs(firstWebPage.title)
+ }.closeTabDrawer {
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ verifyUrl(secondWebPage.url.toString())
+ }.goToHomescreen {
+ verifyJumpBackInSectionIsDisplayed()
+ verifyJumpBackInItemTitle(activityTestRule, secondWebPage.title)
+ verifyJumpBackInItemWithUrl(activityTestRule, secondWebPage.url.toString())
+ }.openTabDrawer {
+ closeTabWithTitle(secondWebPage.title)
+ }.closeTabDrawer {
+ }
+
+ homeScreen {
+ verifyJumpBackInSectionIsDisplayed()
+ verifyJumpBackInItemTitle(activityTestRule, firstWebPage.title)
+ verifyJumpBackInItemWithUrl(activityTestRule, firstWebPage.url.toString())
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ homeScreen {
+ verifyJumpBackInSectionIsNotDisplayed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569839
+ @Test
+ fun verifyCustomizeHomepageButtonTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.goToHomescreen {
+ }.openCustomizeHomepage {
+ clickShortcutsButton()
+ clickJumpBackInButton()
+ clickRecentBookmarksButton()
+ clickRecentSearchesButton()
+ clickPocketButton()
+ }.goBackToHomeScreen {
+ verifyCustomizeHomepageButton(false)
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ clickShortcutsButton()
+ }.goBackToHomeScreen {
+ verifyCustomizeHomepageButton(true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414970
+ @SmokeTest
+ @Test
+ fun addPrivateBrowsingShortcutFromHomeScreenCFRTest() {
+ homeScreen {
+ }.triggerPrivateBrowsingShortcutPrompt {
+ verifyNoThanksPrivateBrowsingShortcutButton(activityTestRule)
+ verifyAddPrivateBrowsingShortcutButton(activityTestRule)
+ clickAddPrivateBrowsingShortcutButton(activityTestRule)
+ clickAddAutomaticallyButton()
+ }.openHomeScreenShortcut("Private ${TestHelper.appName}") {}
+ searchScreen {
+ verifySearchView()
+ }.dismissSearchBar {
+ verifyCommonMythsLink()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569867
+ @Test
+ fun verifyJumpBackInContextualHintTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isJumpBackInCFREnabled = true
+ }
+
+ val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ }.goToHomescreen {
+ verifyJumpBackInMessage(activityTestRule)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/LoginsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/LoginsTest.kt
new file mode 100644
index 0000000000..317aa9c397
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/LoginsTest.kt
@@ -0,0 +1,820 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.os.Build
+import android.view.autofill.AutofillManager
+import androidx.core.net.toUri
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestHelper.waitForAppWindowToBeUpdated
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clearTextFieldItem
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.setPageObjectText
+
+/**
+ * Tests for verifying:
+ * - the Logins and Passwords menu and sub-menus.
+ * - save login prompts.
+ * - saving logins based on the user's preferences.
+ */
+class LoginsTest : TestSetup() {
+ @get:Rule
+ val activityTestRule =
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ val autofillManager: AutofillManager =
+ TestHelper.appContext.getSystemService(AutofillManager::class.java)
+ autofillManager.disableAutofillServices()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092713
+ // Tests the Passwords menu items and default values
+ @Test
+ fun loginsAndPasswordsSettingsItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ // Necessary to scroll a little bit for all screen sizes
+ scrollToElementByText("Passwords")
+ }.openLoginsAndPasswordSubMenu {
+ verifyDefaultView()
+ verifyAutofillInFirefoxToggle(true)
+ verifyAutofillLoginsInOtherAppsToggle(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/517816
+ // Tests only for initial state without signing in.
+ // For tests after signing in, see SyncIntegration test suite
+ @Test
+ fun verifySavedLoginsListTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ // Necessary to scroll a little bit for all screen sizes
+ scrollToElementByText("Passwords")
+ }.openLoginsAndPasswordSubMenu {
+ verifyDefaultView()
+ }.openSavedLogins {
+ verifySecurityPromptForLogins()
+ tapSetupLater()
+ // Verify that logins list is empty
+ verifyEmptySavedLoginsListView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092925
+ @Test
+ fun verifySyncLoginsOptionsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ // Necessary to scroll a little bit for all screen sizes
+ scrollToElementByText("Passwords")
+ }.openLoginsAndPasswordSubMenu {
+ }.openSyncLogins {
+ verifyReadyToScanOption()
+ verifyUseEmailOption()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/523839
+ @Test
+ fun saveLoginFromPromptTest() {
+ val saveLoginTest =
+ TestAssetHelper.getSaveLoginAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSaveLoginsAndPasswordsOptions {
+ verifySaveLoginsOptionsView()
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(saveLoginTest.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ // Click save to save the login
+ clickPageObject(itemWithText("Save"))
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ scrollToElementByText("Passwords")
+ }.openLoginsAndPasswordSubMenu {
+ verifyDefaultView()
+ }.openSavedLogins {
+ verifySecurityPromptForLogins()
+ tapSetupLater()
+ // Verify that the login appears correctly
+ verifySavedLoginsSectionUsername("test@example.com")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/960412
+ @Test
+ fun openLoginWebsiteInBrowserTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
+ val originWebsite = "mozilla-mobile.github.io"
+ val userName = "test"
+ val password = "pass"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), userName)
+ setPageObjectText(itemWithResId("password"), password)
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ verifySecurityPromptForLogins()
+ tapSetupLater()
+ viewSavedLoginDetails(userName)
+ }.goToSavedWebsite {
+ verifyUrl(originWebsite)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/517817
+ @Test
+ fun neverSaveLoginFromPromptTest() {
+ val saveLoginTest = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(saveLoginTest.url) {
+ clickSubmitLoginButton()
+ // Don't save the login, add to exceptions
+ clickPageObject(itemWithText("Never save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ verifyDefaultView()
+ }.openSavedLogins {
+ verifySecurityPromptForLogins()
+ tapSetupLater()
+ // Verify that the login list is empty
+ verifyEmptySavedLoginsListView()
+ verifyNotSavedLoginFromPrompt()
+ }.goBack {
+ }.openLoginExceptions {
+ // Verify localhost was added to exceptions list
+ verifyLocalhostExceptionAdded()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1508171
+ @SmokeTest
+ @Test
+ fun verifyUpdatedLoginIsSavedTest() {
+ val saveLoginTest =
+ TestAssetHelper.getSaveLoginAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(saveLoginTest.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ // Click Save to save the login
+ clickPageObject(itemWithText("Save"))
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(saveLoginTest.url) {
+ enterPassword("test")
+ mDevice.waitForIdle()
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ // Click Update to change the saved password
+ clickPageObject(itemWithText("Update"))
+ }.openThreeDotMenu {
+ }.openSettings {
+ scrollToElementByText("Passwords")
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ verifySecurityPromptForLogins()
+ tapSetupLater()
+ // Verify that the login appears correctly
+ verifySavedLoginsSectionUsername("test@example.com")
+ viewSavedLoginDetails("test@example.com")
+ revealPassword()
+ verifyPasswordSaved("test") // failing here locally
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1049971
+ @SmokeTest
+ @Test
+ fun verifyMultipleLoginsSelectionsTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+ val firstUser = "mozilla"
+ val firstPass = "firefox"
+ val secondUser = "fenix"
+ val secondPass = "pass"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), firstUser)
+ waitForAppWindowToBeUpdated()
+ setPageObjectText(itemWithResId("password"), firstPass)
+ waitForAppWindowToBeUpdated()
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ setPageObjectText(itemWithResId("username"), secondUser)
+ waitForAppWindowToBeUpdated()
+ setPageObjectText(itemWithResId("password"), secondPass)
+ waitForAppWindowToBeUpdated()
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ clearTextFieldItem(itemWithResId("username"))
+ clickSuggestedLoginsButton()
+ verifySuggestedUserName(firstUser)
+ verifySuggestedUserName(secondUser)
+ clickPageObject(
+ itemWithResIdAndText(
+ "$packageName:id/username",
+ firstUser,
+ ),
+ )
+ clickPageObject(itemWithResId("togglePassword"))
+ verifyPrefilledLoginCredentials(firstUser, firstPass, true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/875849
+ @Test
+ fun verifyEditLoginsViewTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails(originWebsite)
+ clickThreeDotButton(activityTestRule)
+ clickEditLoginButton()
+ setNewPassword("fenix")
+ saveEditedLogin()
+ revealPassword()
+ verifyPasswordSaved("fenix")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/875851
+ @Test
+ fun verifyEditedLoginsAreSavedTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails(originWebsite)
+ clickThreeDotButton(activityTestRule)
+ clickEditLoginButton()
+ setNewUserName("android")
+ setNewPassword("fenix")
+ saveEditedLogin()
+ }
+
+ exitMenu()
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ clickPageObject(itemWithResId("togglePassword"))
+ verifyPrefilledLoginCredentials("android", "fenix", true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2266452
+ @Test
+ fun verifyLoginWithNoUserNameCanNotBeSavedTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails(originWebsite)
+ clickThreeDotButton(activityTestRule)
+ clickEditLoginButton()
+ clickClearUserNameButton()
+ verifyUserNameRequiredErrorMessage()
+ verifySaveLoginButtonIsEnabled(false)
+ clickGoBackButton()
+ verifyLoginItemUsername("mozilla")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2266453
+ @Test
+ fun verifyLoginWithoutPasswordCanNotBeSavedTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails(originWebsite)
+ clickThreeDotButton(activityTestRule)
+ clickEditLoginButton()
+ clickClearPasswordButton()
+ verifyPasswordRequiredErrorMessage()
+ verifySaveLoginButtonIsEnabled(false)
+ clickGoBackButton()
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/876531
+ @Test
+ fun verifyEditModeDismissalDoesNotSaveLoginCredentialsTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails(originWebsite)
+ clickThreeDotButton(activityTestRule)
+ clickEditLoginButton()
+ setNewUserName("android")
+ setNewPassword("fenix")
+ clickGoBackButton()
+ verifyLoginItemUsername("mozilla")
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/876532
+ @Test
+ fun verifyDeleteLoginButtonTest() {
+ val loginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.url) {
+ clickSubmitLoginButton()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails("test@example.com")
+ clickThreeDotButton(activityTestRule)
+ clickDeleteLoginButton()
+ verifyLoginDeletionPrompt()
+ clickCancelDeleteLogin()
+ verifyLoginItemUsername("test@example.com")
+ viewSavedLoginDetails("test@example.com")
+ clickThreeDotButton(activityTestRule)
+ clickDeleteLoginButton()
+ verifyLoginDeletionPrompt()
+ clickConfirmDeleteLogin()
+ // The account remains displayed, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812431
+ // verifyNotSavedLoginFromPrompt()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/517818
+ @SmokeTest
+ @Test
+ fun verifyNeverSaveLoginOptionTest() {
+ val loginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSaveLoginsAndPasswordsOptions {
+ clickNeverSaveOption()
+ }.goBack {
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsNotDisplayed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/517819
+ @Test
+ fun verifyAutofillToggleTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ waitForPageToLoad()
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ waitForAppWindowToBeUpdated()
+ setPageObjectText(itemWithResId("password"), "firefox")
+ waitForAppWindowToBeUpdated()
+ clickPageObject(itemWithResId("submit"))
+ waitForPageToLoad()
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ waitForPageToLoad()
+ clickPageObject(itemWithResId("togglePassword"))
+ verifyPrefilledLoginCredentials("mozilla", "firefox", true)
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ verifyAutofillInFirefoxToggle(true)
+ clickAutofillInFirefoxOption()
+ verifyAutofillInFirefoxToggle(false)
+ }.goBack {
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ verifyPrefilledLoginCredentials("mozilla", "firefox", false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/593768
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1812995")
+ @Test
+ fun doNotSaveOptionWillNotUpdateALoginTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ clickPageObject(itemWithResId("togglePassword"))
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "fenix")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Don’t update"))
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails(originWebsite)
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2090455
+ @Test
+ fun searchLoginsByUsernameTest() {
+ val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+ val secondLoginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstLoginPage.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondLoginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "android")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ clickSearchLoginButton()
+ searchLogin("ANDROID")
+ viewSavedLoginDetails(originWebsite)
+ verifyLoginItemUsername("android")
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }.goBackToSavedLogins {
+ searchLogin("android")
+ viewSavedLoginDetails(originWebsite)
+ verifyLoginItemUsername("android")
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }.goBackToSavedLogins {
+ searchLogin("AnDrOiD")
+ viewSavedLoginDetails(originWebsite)
+ verifyLoginItemUsername("android")
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/608834
+ @Test
+ fun searchLoginsByUrlTest() {
+ val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+ val secondLoginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstLoginPage.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondLoginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "android")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ clickSearchLoginButton()
+ searchLogin("MOZILLA")
+ viewSavedLoginDetails(originWebsite)
+ verifyLoginItemUsername("android")
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }.goBackToSavedLogins {
+ searchLogin("mozilla")
+ viewSavedLoginDetails(originWebsite)
+ verifyLoginItemUsername("android")
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }.goBackToSavedLogins {
+ searchLogin("MoZiLlA")
+ viewSavedLoginDetails(originWebsite)
+ verifyLoginItemUsername("android")
+ revealPassword()
+ verifyPasswordSaved("firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2266441
+ @Test
+ fun verifyLastUsedLoginSortingOptionTest() {
+ val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+ val secondLoginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstLoginPage.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondLoginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ clickSavedLoginsChevronIcon()
+ verifyLoginsSortingOptions()
+ clickLastUsedSortingOption()
+ verifySortedLogin(0, originWebsite)
+ verifySortedLogin(1, firstLoginPage.url.authority.toString())
+ }.goBack {
+ }.openSavedLogins {
+ verifySortedLogin(0, originWebsite)
+ verifySortedLogin(1, firstLoginPage.url.authority.toString())
+ }
+
+ restartApp(activityTestRule)
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ verifySortedLogin(0, originWebsite)
+ verifySortedLogin(1, firstLoginPage.url.authority.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2266442
+ @Test
+ fun verifyAlphabeticalLoginSortingOptionTest() {
+ val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+ val secondLoginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+ val originWebsite = "mozilla-mobile.github.io"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstLoginPage.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondLoginPage.toUri()) {
+ setPageObjectText(itemWithResId("username"), "mozilla")
+ setPageObjectText(itemWithResId("password"), "firefox")
+ clickPageObject(itemWithResId("submit"))
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ verifySortedLogin(0, firstLoginPage.url.authority.toString())
+ verifySortedLogin(1, originWebsite)
+ }.goBack {
+ }.openSavedLogins {
+ verifySortedLogin(0, firstLoginPage.url.authority.toString())
+ verifySortedLogin(1, originWebsite)
+ }
+
+ restartApp(activityTestRule)
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ verifySortedLogin(0, firstLoginPage.url.authority.toString())
+ verifySortedLogin(1, originWebsite)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1518435
+ @Test
+ fun verifyAddLoginManuallyTest() {
+ val loginPage = "https://mozilla-mobile.github.io/testapp/v2.0/loginForm.html"
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ clickAddLoginButton()
+ verifyAddNewLoginView()
+ enterSiteCredential("mozilla")
+ verifyHostnameErrorMessage()
+ enterSiteCredential(loginPage)
+ verifyHostnameClearButtonEnabled()
+ setNewUserName("mozilla")
+ setNewPassword("firefox")
+ clickClearPasswordButton()
+ verifyPasswordErrorMessage()
+ setNewPassword("firefox")
+ verifyPasswordClearButtonEnabled()
+ saveEditedLogin()
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(loginPage.toUri()) {
+ clickPageObject(MatcherHelper.itemWithResId("username"))
+ clickSuggestedLoginsButton()
+ verifySuggestedUserName("mozilla")
+ clickPageObject(itemWithResIdAndText("$packageName:id/username", "mozilla"))
+ clickPageObject(itemWithResId("togglePassword"))
+ verifyPrefilledLoginCredentials("mozilla", "firefox", true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2068215
+ @Test
+ fun verifyCopyLoginCredentialsToClipboardTest() {
+ val firstLoginPage = TestAssetHelper.getSaveLoginAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstLoginPage.url) {
+ clickSubmitLoginButton()
+ verifySaveLoginPromptIsDisplayed()
+ clickPageObject(itemWithText("Save"))
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLoginsAndPasswordSubMenu {
+ }.openSavedLogins {
+ tapSetupLater()
+ viewSavedLoginDetails("test@example.com")
+ clickCopyUserNameButton()
+ verifySnackBarText("Username copied to clipboard")
+ clickCopyPasswordButton()
+ verifySnackBarText("Password copied to clipboard")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/MainMenuTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/MainMenuTest.kt
new file mode 100644
index 0000000000..b4646ece93
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/MainMenuTest.kt
@@ -0,0 +1,386 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import mozilla.components.concept.engine.utils.EngineReleaseChannel
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertYoutubeAppOpens
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
+import org.mozilla.fenix.helpers.Constants.PackageName.PRINT_SPOOLER
+import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.nimbus.FxNimbus
+import org.mozilla.fenix.nimbus.Translations
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickContextMenuItem
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.longClickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class MainMenuTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233849
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ FxNimbus.features.translations.withInitializer { _, _ ->
+ // These are FML generated objects and enums
+ Translations(
+ mainFlowToolbarEnabled = true,
+ mainFlowBrowserMenuEnabled = true,
+ pageSettingsEnabled = true,
+ globalSettingsEnabled = true,
+ globalLangSettingsEnabled = true,
+ globalSiteSettingsEnabled = true,
+ downloadsEnabled = true,
+ )
+ }
+ }
+
+ @Test
+ fun verifyTabMainMenuItemsTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled = false)
+ }
+ }
+
+ // Verifies the list of items in the homescreen's 3 dot main menu
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233848
+ @SmokeTest
+ @Test
+ fun homeMainMenuItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled = false)
+ }.openBookmarks {
+ verifyBookmarksMenuView()
+ }.goBack {
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryMenuView()
+ }.goBack {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyEmptyDownloadsList()
+ }.goBack {
+ }.openThreeDotMenu {
+ }.openAddonsManagerMenu {
+ verifyAddonsListIsDisplayed(true)
+ }.goBack {
+ }.openThreeDotMenu {
+ }.openSyncSignIn {
+ verifyTurnOnSyncMenu()
+ }.goBack {}
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openWhatsNew {
+ verifyWhatsNewURL()
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ }.openHelp {
+ verifyHelpUrl()
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ verifyHomePageView()
+ }.goBackToHomeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2284134
+ @Test
+ fun openNewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.clickNewTabButton {
+ verifySearchView()
+ }.submitQuery("test") {
+ verifyTabCounter("2")
+ }
+ }
+
+ // Device or AVD requires a Google Services Android OS installation with Play Store installed
+ // Verifies the Open in app button when an app is installed
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/387756
+ @SmokeTest
+ @Test
+ fun openInAppFunctionalityTest() {
+ val youtubeURL = "vnd.youtube://".toUri()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(youtubeURL) {
+ verifyNotificationDotOnMainMenu()
+ }.openThreeDotMenu {
+ }.clickOpenInApp {
+ assertYoutubeAppOpens()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2284323
+ @Test
+ fun openSyncAndSaveDataTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSyncSignIn {
+ verifyTurnOnSyncMenu()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243840
+ @Test
+ fun findInPageTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ verifyThreeDotMenuExists()
+ verifyFindInPageButton()
+ }.openFindInPage {
+ verifyFindInPageNextButton()
+ verifyFindInPagePrevButton()
+ verifyFindInPageCloseButton()
+ enterFindInPageQuery("a")
+ verifyFindInPageResult("1/3")
+ clickFindInPageNextButton()
+ verifyFindInPageResult("2/3")
+ clickFindInPageNextButton()
+ verifyFindInPageResult("3/3")
+ clickFindInPagePrevButton()
+ verifyFindInPageResult("2/3")
+ clickFindInPagePrevButton()
+ verifyFindInPageResult("1/3")
+ }.closeFindInPageWithCloseButton {
+ verifyFindInPageBar(false)
+ }.openThreeDotMenu {
+ }.openFindInPage {
+ enterFindInPageQuery("3")
+ verifyFindInPageResult("1/1")
+ }.closeFindInPageWithBackButton {
+ verifyFindInPageBar(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2283303
+ @Test
+ fun switchDesktopSiteModeOnOffTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.switchDesktopSiteMode {
+ }.openThreeDotMenu {
+ verifyDesktopSiteModeEnabled(true)
+ }.switchDesktopSiteMode {
+ }.openThreeDotMenu {
+ verifyDesktopSiteModeEnabled(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1314137
+ @Test
+ fun setDesktopSiteBeforePageLoadTest() {
+ val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ verifyDesktopSiteModeEnabled(false)
+ }.switchDesktopSiteMode {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ }.openThreeDotMenu {
+ verifyDesktopSiteModeEnabled(true)
+ }.closeBrowserMenuToBrowser {
+ clickPageObject(MatcherHelper.itemContainingText("Link 1"))
+ }.openThreeDotMenu {
+ verifyDesktopSiteModeEnabled(true)
+ }.closeBrowserMenuToBrowser {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(webPage.url) {
+ longClickPageObject(MatcherHelper.itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ TestHelper.clickSnackbarButton("SWITCH")
+ }.openThreeDotMenu {
+ verifyDesktopSiteModeEnabled(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2283302
+ @Test
+ fun reportSiteIssueTest() {
+ runWithCondition(
+ // This test will not run on RC builds because the "Report site issue button" is not available.
+ activityTestRule.activity.components.core.engine.version.releaseChannel !== EngineReleaseChannel.RELEASE,
+ ) {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.openReportSiteIssue {
+ verifyUrl("webcompat.com/issues/new")
+ }
+ }
+ }
+
+ // Verifies the Add to home screen option in a tab's 3 dot menu
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/410724
+ @SmokeTest
+ @Test
+ fun addPageShortcutToHomeScreenTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val shortcutTitle = generateRandomString(5)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(website.url) {
+ }.openThreeDotMenu {
+ expandMenu()
+ }.openAddToHomeScreen {
+ clickCancelShortcutButton()
+ }
+
+ browserScreen {
+ }.openThreeDotMenu {
+ expandMenu()
+ }.openAddToHomeScreen {
+ verifyShortcutTextFieldTitle("Test_Page_1")
+ addShortcutName(shortcutTitle)
+ clickAddShortcutButton()
+ clickAddAutomaticallyButton()
+ }.openHomeScreenShortcut(shortcutTitle) {
+ verifyUrl(website.url.toString())
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/329893
+ @SmokeTest
+ @Test
+ fun mainMenuShareButtonTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.clickShareButton {
+ verifyShareTabLayout()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233604
+ @Test
+ fun navigateBackAndForwardTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val nextWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.waitForIdle()
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(nextWebPage.url) {
+ verifyUrl(nextWebPage.url.toString())
+ }.openThreeDotMenu {
+ }.goToPreviousPage {
+ mDevice.waitForIdle()
+ verifyUrl(defaultWebPage.url.toString())
+ }.openThreeDotMenu {
+ }.goForward {
+ verifyUrl(nextWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2195819
+ @SmokeTest
+ @Test
+ fun refreshPageButtonTest() {
+ val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(refreshWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ verifyThreeDotMenuExists()
+ }.refreshPage {
+ verifyPageContent("REFRESHED")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2265657
+ @Test
+ fun forceRefreshPageTest() {
+ val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(refreshWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ verifyThreeDotMenuExists()
+ }.forceRefreshPage {
+ verifyPageContent("REFRESHED")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2282411
+ @Test
+ fun printWebPageFromMainMenuTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.clickPrintButton {
+ assertNativeAppOpens(PRINT_SPOOLER)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2282408
+ @Test
+ fun printWebPageFromShareMenuTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.clickShareButton {
+ }.clickPrintButton {
+ assertNativeAppOpens(PRINT_SPOOLER)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt
new file mode 100644
index 0000000000..33304899c9
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.notificationShade
+
+/**
+ * Tests for verifying basic functionality of media notifications:
+ * - video and audio playback system notifications appear and can pause/play the media content
+ * - a media notification icon is displayed on the homescreen for the tab playing media content
+ * Note: this test only verifies media notifications, not media itself
+ */
+class MediaNotificationTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1347033
+ @SmokeTest
+ @Test
+ fun verifyVideoPlaybackSystemNotificationTest() {
+ val videoTestPage = TestAssetHelper.getVideoPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(videoTestPage.url) {
+ mDevice.waitForIdle()
+ clickPageObject(itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openNotificationShade {
+ verifySystemNotificationExists(videoTestPage.title)
+ clickMediaNotificationControlButton("Pause")
+ verifyMediaSystemNotificationButtonState("Play")
+ }
+
+ mDevice.pressBack()
+
+ browserScreen {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ mDevice.openNotification()
+
+ notificationShade {
+ verifySystemNotificationDoesNotExist(videoTestPage.title)
+ }
+
+ // close notification shade before the next test
+ mDevice.pressBack()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2316010
+ @SmokeTest
+ @Test
+ fun verifyAudioPlaybackSystemNotificationTest() {
+ val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(audioTestPage.url) {
+ mDevice.waitForIdle()
+ clickPageObject(itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openNotificationShade {
+ verifySystemNotificationExists(audioTestPage.title)
+ clickMediaNotificationControlButton("Pause")
+ verifyMediaSystemNotificationButtonState("Play")
+ }
+
+ mDevice.pressBack()
+
+ browserScreen {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }.openTabDrawer {
+ closeTab()
+ }
+
+ mDevice.openNotification()
+
+ notificationShade {
+ verifySystemNotificationDoesNotExist(audioTestPage.title)
+ }
+
+ // close notification shade before the next test
+ mDevice.pressBack()
+ }
+
+ // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903595
+ @Test
+ fun mediaSystemNotificationInPrivateModeTest() {
+ val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.openTabTray {
+ }.toggleToPrivateTabs {
+ }.openNewTab {
+ }.submitQuery(audioTestPage.url.toString()) {
+ mDevice.waitForIdle()
+ clickPageObject(itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openNotificationShade {
+ verifySystemNotificationExists("A site is playing media")
+ clickMediaNotificationControlButton("Pause")
+ verifyMediaSystemNotificationButtonState("Play")
+ }
+
+ mDevice.pressBack()
+
+ browserScreen {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }.openTabDrawer {
+ closeTab()
+ verifySnackBarText("Private tab closed")
+ }
+
+ mDevice.openNotification()
+
+ notificationShade {
+ verifySystemNotificationDoesNotExist("A site is playing media")
+ }
+
+ // close notification shade before and go back to regular mode before the next test
+ mDevice.pressBack()
+ homeScreen { }.togglePrivateBrowsingMode()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ModifierTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ModifierTest.kt
new file mode 100644
index 0000000000..667cea647a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ModifierTest.kt
@@ -0,0 +1,119 @@
+package org.mozilla.fenix.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performScrollToIndex
+import androidx.compose.ui.unit.dp
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.compose.ext.onShown
+
+private const val ON_SHOWN_ROOT_TAG = "onShownRoot"
+private const val ON_SHOWN_SETTLE_TIME_MS = 1000
+private const val ON_SHOWN_INDEX = 15
+private const val ON_SHOWN_NODE_COUNT = 30
+
+class ModifierTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun verifyModifierOnShownWhenScrolledToWithNoSettleTime() {
+ var onShown = false
+ composeTestRule.setContent {
+ ModifierOnShownContent(
+ settleTime = 0,
+ onVisible = {
+ onShown = true
+ },
+ )
+ }
+
+ composeTestRule.scrollToOnShownIndex()
+
+ assertTrue(onShown)
+ }
+
+ @Test
+ fun verifyModifierOnShownAfterSettled() {
+ var onShown = false
+ composeTestRule.setContent {
+ ModifierOnShownContent(
+ onVisible = {
+ onShown = true
+ },
+ )
+ }
+
+ composeTestRule.scrollToOnShownIndex()
+
+ assertFalse(onShown)
+
+ composeTestRule.waitUntil(ON_SHOWN_SETTLE_TIME_MS + 500L) { onShown }
+
+ assertTrue(onShown)
+ }
+
+ @Test
+ fun verifyModifierOnShownWhenNotVisible() {
+ val indexToValidate = ON_SHOWN_NODE_COUNT - 1
+ var onShown = false
+ composeTestRule.setContent {
+ ModifierOnShownContent(
+ indexToValidate = indexToValidate,
+ settleTime = 0,
+ onVisible = {
+ onShown = true
+ },
+ )
+ }
+
+ assertFalse(onShown)
+ }
+
+ private fun ComposeTestRule.scrollToOnShownIndex(index: Int = ON_SHOWN_INDEX) {
+ this.onNodeWithTag(ON_SHOWN_ROOT_TAG)
+ .performScrollToIndex(index)
+ }
+
+ @Composable
+ private fun ModifierOnShownContent(
+ indexToValidate: Int = ON_SHOWN_INDEX,
+ settleTime: Int = ON_SHOWN_SETTLE_TIME_MS,
+ onVisible: () -> Unit,
+ ) {
+ LazyColumn(
+ modifier = Modifier.testTag(ON_SHOWN_ROOT_TAG),
+ ) {
+ items(ON_SHOWN_NODE_COUNT) { index ->
+ val modifier = if (index == indexToValidate) {
+ Modifier.onShown(
+ threshold = 1.0f,
+ settleTime = settleTime,
+ onVisible = onVisible,
+ )
+ } else {
+ Modifier
+ }
+
+ Text(
+ text = "Test item $index",
+ modifier = modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ )
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt
new file mode 100644
index 0000000000..a486a4e676
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import java.util.Locale
+
+/**
+ * Tests for verifying basic functionality of browser navigation and page related interactions
+ *
+ * Including:
+ * - Visiting a URL
+ * - Back and Forward navigation
+ * - Refresh
+ * - Find in page
+ */
+
+class NavigationToolbarTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326
+ // Swipes the nav bar left/right to switch between tabs
+ @SmokeTest
+ @Test
+ fun swipeToSwitchTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ swipeNavBarRight(secondWebPage.url.toString())
+ verifyUrl(firstWebPage.url.toString())
+ swipeNavBarLeft(firstWebPage.url.toString())
+ verifyUrl(secondWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987327
+ // Because it requires changing system prefs, this test will run only on Debug builds
+ @Test
+ fun swipeToSwitchTabInRTLTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val arabicLocale = Locale("ar", "AR")
+
+ runWithSystemLocaleChanged(arabicLocale, activityTestRule) {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ swipeNavBarLeft(secondWebPage.url.toString())
+ verifyUrl(firstWebPage.url.toString())
+ swipeNavBarRight(firstWebPage.url.toString())
+ verifyUrl(secondWebPage.url.toString())
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2265279
+ @SmokeTest
+ @Test
+ fun verifySecurePageSecuritySubMenuTest() {
+ val defaultWebPage = "https://mozilla-mobile.github.io/testapp/loginForm"
+ val defaultWebPageTitle = "Login_form"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.toUri()) {
+ waitForPageToLoad()
+ }.openSiteSecuritySheet {
+ verifyQuickActionSheet(defaultWebPage, true)
+ openSecureConnectionSubMenu(true)
+ verifySecureConnectionSubMenu(defaultWebPageTitle, defaultWebPage, true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2265280
+ @SmokeTest
+ @Test
+ fun verifyInsecurePageSecuritySubMenuTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }.openSiteSecuritySheet {
+ verifyQuickActionSheet(defaultWebPage.url.toString(), false)
+ openSecureConnectionSubMenu(false)
+ verifySecureConnectionSubMenu(defaultWebPage.title, defaultWebPage.url.toString(), false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1661318
+ @SmokeTest
+ @Test
+ fun verifyClearCookiesFromQuickSettingsTest() {
+ val helpPageUrl = "mozilla.org"
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHelp {
+ }.openSiteSecuritySheet {
+ clickQuickActionSheetClearSiteData()
+ verifyClearSiteDataPrompt(helpPageUrl)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1360555
+ @SmokeTest
+ @Test
+ fun goToHomeScreenTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ mDevice.waitForIdle()
+ }.goToHomescreen {
+ verifyHomeScreen()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2256552
+ @SmokeTest
+ @Test
+ fun goToHomeScreenInPrivateModeTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ togglePrivateBrowsingModeOnOff()
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ mDevice.waitForIdle()
+ }.goToHomescreen {
+ verifyHomeScreen()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusEventTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusEventTest.kt
new file mode 100644
index 0000000000..943e0624c8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusEventTest.kt
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.content.Intent
+import io.mockk.mockk
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.service.fxa.FirefoxAccount
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.components.TelemetryAccountObserver
+import org.mozilla.fenix.helpers.Experimentation
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestSetup
+
+class NimbusEventTest : TestSetup() {
+ @get:Rule
+ val homeActivityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+ .withIntent(
+ Intent().apply {
+ action = Intent.ACTION_VIEW
+ },
+ )
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ @Test
+ fun homeScreenNimbusEventsTest() {
+ Experimentation.withHelper {
+ assertTrue(evalJexl("'app_opened'|eventSum('Days', 28, 0) > 0"))
+ }
+ }
+
+ @Test
+ fun telemetryAccountObserverTest() {
+ val observer = TelemetryAccountObserver(appContext)
+ // replacing interface mock with implementation mock.
+ observer.onAuthenticated(mockk(), AuthType.Signin)
+
+ Experimentation.withHelper {
+ assertTrue(evalJexl("'sync_auth.sign_in'|eventSum('Days', 28, 0) > 0"))
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingHomescreenTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingHomescreenTest.kt
new file mode 100644
index 0000000000..95ced206a2
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingHomescreenTest.kt
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.content.Intent
+import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
+import mozilla.components.service.nimbus.messaging.MessageData
+import mozilla.components.service.nimbus.messaging.Messaging
+import mozilla.components.service.nimbus.messaging.StyleData
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.experiments.nimbus.Res
+import org.mozilla.fenix.FenixApplication
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.nimbus.FxNimbus
+import org.mozilla.fenix.nimbus.HomeScreenSection
+import org.mozilla.fenix.nimbus.Homescreen
+import org.mozilla.fenix.ui.robots.homeScreen
+
+/**
+ * Tests for verifying basic functionality of the Nimbus Home Screen message
+ *
+ * Verifies a message can be displayed with all of the correct components
+**/
+
+class NimbusMessagingHomescreenTest : TestSetup() {
+ private var messageButtonLabel = "CLICK ME"
+ private var messageText = "Some Nimbus Messaging text"
+ private var messageTitle = "A Nimbus title"
+
+ @get:Rule
+ val homeActivityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(
+ skipOnboarding = true,
+ ).withIntent(
+ Intent().apply {
+ action = Intent.ACTION_VIEW
+ },
+ )
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ // Set up nimbus message
+ FxNimbusMessaging.features.messaging.withInitializer { _, _ ->
+ // FML generated objects.
+ Messaging(
+ messages = mapOf(
+ "test-message" to MessageData(
+ action = "TEST ACTION",
+ style = "TEST STYLE",
+ buttonLabel = Res.string(messageButtonLabel),
+ text = Res.string(messageText),
+ title = Res.string(messageTitle),
+ triggerIfAll = listOf("ALWAYS"),
+ ),
+ ),
+ styles = mapOf(
+ "TEST STYLE" to StyleData(),
+ ),
+ actions = mapOf(
+ "TEST ACTION" to "https://example.com",
+ ),
+ triggers = mapOf(
+ "ALWAYS" to "true",
+ ),
+ )
+ }
+
+ // Remove some homescreen features not needed for testing
+ FxNimbus.features.homescreen.withInitializer { _, _ ->
+ // These are FML generated objects and enums
+ Homescreen(
+ sectionsEnabled = mapOf(
+ HomeScreenSection.JUMP_BACK_IN to false,
+ HomeScreenSection.POCKET to false,
+ HomeScreenSection.POCKET_SPONSORED_STORIES to false,
+ HomeScreenSection.RECENT_EXPLORATIONS to false,
+ HomeScreenSection.RECENTLY_SAVED to false,
+ HomeScreenSection.TOP_SITES to false,
+ ),
+ )
+ }
+ // refresh message store
+ val application = (homeActivityTestRule.activity.application as FenixApplication)
+ application.restoreMessaging()
+ }
+
+ @Test
+ fun testNimbusMessageIsDisplayed() {
+ // Checks the home screen card message is displayed correctly
+ homeScreen {
+ verifyNimbusMessageCard(messageTitle, messageText, messageButtonLabel)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingMessageTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingMessageTest.kt
new file mode 100644
index 0000000000..0ca94358e8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingMessageTest.kt
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.content.Context
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
+import mozilla.components.service.nimbus.messaging.Messaging
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestSetup
+
+/**
+ * This test is to test the integrity of messages hardcoded in the FML.
+ *
+ * It tests if the trigger expressions are valid, all the fields are complete
+ * and a simple check if they are localized (don't contain `_`).
+ */
+class NimbusMessagingMessageTest : TestSetup() {
+ private lateinit var feature: Messaging
+ private lateinit var context: Context
+
+ private val messaging
+ get() = context.components.nimbus.messaging
+
+ @get:Rule
+ val activityTestRule =
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ context = TestHelper.appContext
+ feature = FxNimbusMessaging.features.messaging.value()
+ }
+
+ /**
+ * Check if all messages in the FML are internally consistent with the
+ * rest of the FML. This check is done in the `NimbusMessagingStorage`
+ * class.
+ */
+ @Test
+ fun testAllMessageIntegrity() = runTest {
+ val messages = messaging.getMessages()
+ val rawMessages = feature.messages
+ assertTrue(rawMessages.isNotEmpty())
+
+ if (messages.size != rawMessages.size) {
+ val expected = rawMessages.keys.toHashSet()
+ val observed = messages.map { it.id }.toHashSet()
+ val missing = expected - observed
+ fail("Problem with message(s) in FML: $missing")
+ }
+ assertEquals(messages.size, rawMessages.size)
+ }
+
+ private fun checkIsLocalized(string: String) {
+ assertFalse(string.isBlank())
+ // The check will almost always succeed, since the generated code
+ // will not compile if this is true, and there is no resource available.
+ assertFalse(string.matches(Regex("[a-z][_a-z\\d]*")))
+ }
+
+ /**
+ * Check that the messages are localized.
+ */
+ @Test
+ fun testAllMessagesAreLocalized() {
+ feature.messages.values.forEach { message ->
+ message.buttonLabel?.let(::checkIsLocalized)
+ message.title?.let(::checkIsLocalized)
+ checkIsLocalized(message.text)
+ }
+ }
+
+ @Test
+ fun testIndividualMessagesAreValid() {
+ val expectedMessages = listOf(
+ "default-browser",
+ "default-browser-notification",
+ )
+ val rawMessages = feature.messages
+ for (id in expectedMessages) {
+ assertTrue(rawMessages.containsKey(id))
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingNotificationTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingNotificationTest.kt
new file mode 100644
index 0000000000..1348ff0071
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingNotificationTest.kt
@@ -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.ui
+
+import android.content.Context
+import android.os.Build
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.rule.GrantPermissionRule.grant
+import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.experiments.nimbus.HardcodedNimbusFeatures
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.nimbus.FxNimbus
+import org.mozilla.fenix.ui.robots.notificationShade
+
+/**
+ * A UI test for testing the notification surface for Nimbus Messaging.
+ */
+class NimbusMessagingNotificationTest : TestSetup() {
+ private lateinit var context: Context
+ private lateinit var hardcodedNimbus: HardcodedNimbusFeatures
+
+ @get:Rule
+ val activityTestRule =
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ @get:Rule
+ val grantPermissionRule: GrantPermissionRule =
+ if (Build.VERSION.SDK_INT >= 33) {
+ grant("android.permission.POST_NOTIFICATIONS")
+ } else {
+ grant()
+ }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ context = TestHelper.appContext
+ }
+
+ @Test
+ fun testShowingNotificationMessage() {
+ hardcodedNimbus = HardcodedNimbusFeatures(
+ context,
+ "messaging" to JSONObject(
+ """
+ {
+ "message-under-experiment": "test-default-browser-notification",
+ "messages": {
+ "test-default-browser-notification": {
+ "title": "preferences_set_as_default_browser",
+ "text": "default_browser_experiment_card_text",
+ "surface": "notification",
+ "style": "NOTIFICATION",
+ "action": "MAKE_DEFAULT_BROWSER",
+ "trigger": [
+ "ALWAYS"
+ ]
+ }
+ }
+ }
+ """.trimIndent(),
+ ),
+ )
+ // The scheduling of the Messaging Notification Worker happens in the HomeActivity
+ // onResume().
+ // We need to have connected FxNimbus to hardcodedNimbus by the time it is scheduled, so
+ // we finishActivity, connect, _then_ re-launch the activity so that the worker has
+ // hardcodedNimbus by the time its re-scheduled.
+ // Because the scheduling happens for a second time, the work request needs to replace the
+ // existing one.
+ activityTestRule.finishActivity()
+ hardcodedNimbus.connectWith(FxNimbus)
+ activityTestRule.launchActivity(null)
+
+ mDevice.openNotification()
+ notificationShade {
+ val data =
+ FxNimbusMessaging.features.messaging.value().messages["test-default-browser-notification"]
+ verifySystemNotificationExists(data!!.title!!)
+ verifySystemNotificationExists(data.text)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingTriggerTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingTriggerTest.kt
new file mode 100644
index 0000000000..31be3ec83f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingTriggerTest.kt
@@ -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 http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
+import mozilla.components.service.nimbus.messaging.Messaging
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.experiments.nimbus.NimbusInterface
+import org.mozilla.experiments.nimbus.internal.NimbusException
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.messaging.CustomAttributeProvider
+
+/**
+ * Test to instantiate Nimbus and automatically test all trigger expressions shipping with the app.
+ *
+ * We do this as a UI test to make sure that:
+ * - as much of the custom targeting and trigger attributes are recorded as possible.
+ * - we can run the Rust JEXL evaluator.
+ */
+class NimbusMessagingTriggerTest : TestSetup() {
+ private lateinit var feature: Messaging
+ private lateinit var nimbus: NimbusInterface
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ nimbus = TestHelper.appContext.components.nimbus.sdk
+ feature = FxNimbusMessaging.features.messaging.value()
+ }
+
+ @Test
+ fun testAllMessageTriggersAreValid() {
+ val triggers = feature.triggers
+ val customAttributes = CustomAttributeProvider.getCustomAttributes(TestHelper.appContext)
+ val jexl = nimbus.createMessageHelper(customAttributes)
+
+ val failed = mutableMapOf()
+ triggers.forEach { (key, expr) ->
+ try {
+ jexl.evalJexl(expr)
+ } catch (e: NimbusException) {
+ failed.put(key, expr)
+ }
+ }
+ if (failed.isNotEmpty()) {
+ Assert.fail("Expressions failed: $failed")
+ }
+ }
+
+ @Test
+ fun testBadTriggersAreDetected() {
+ val jexl = nimbus.createMessageHelper()
+
+ val triggers = mapOf(
+ "Syntax error" to "|'syntax error'|",
+ "Invalid identifier" to "invalid_identifier",
+ "Invalid transform" to "'string'|invalid_transform",
+ "Invalid interval" to "'string'|eventLastSeen('Invalid')",
+ )
+
+ triggers.forEach { (key, expr) ->
+ try {
+ jexl.evalJexl(expr)
+ Assert.fail("$key expression failed to error: $expr")
+ } catch (e: NimbusException) {
+ // NOOP
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt
new file mode 100644
index 0000000000..ce59c9e561
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.verifyUrl
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests to verify some main UI flows with Network connection off
+ *
+ */
+
+class NoNetworkAccessStartupTests : TestSetup() {
+
+ @get:Rule
+ val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides(launchActivity = false)
+
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ // Based on STR from https://github.com/mozilla-mobile/fenix/issues/16886
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2240542
+ @Test
+ fun noNetworkConnectionStartupTest() {
+ setNetworkEnabled(false)
+
+ activityTestRule.launchActivity(null)
+
+ homeScreen {
+ verifyHomeScreen()
+ }
+ }
+
+ // Based on STR from https://github.com/mozilla-mobile/fenix/issues/16886
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2240722
+ @Test
+ fun networkInterruptedFromBrowserToHomeTest() {
+ val url = "example.com"
+
+ activityTestRule.launchActivity(null)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(url.toUri()) {}
+
+ setNetworkEnabled(false)
+
+ browserScreen {
+ }.goToHomescreen {
+ verifyHomeScreen()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2240723
+ @Test
+ fun testPageReloadAfterNetworkInterrupted() {
+ val url = "example.com"
+
+ activityTestRule.launchActivity(null)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(url.toUri()) {}
+
+ setNetworkEnabled(false)
+
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage { }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2240721
+ @SmokeTest
+ @Test
+ fun testSignInPageWithNoNetworkConnection() {
+ setNetworkEnabled(false)
+
+ activityTestRule.launchActivity(null)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openTurnOnSyncMenu {
+ tapOnUseEmailToSignIn()
+ verifyUrl(
+ "firefox.com",
+ "$packageName:id/mozac_browser_toolbar_url_view",
+ R.id.mozac_browser_toolbar_url_view,
+ )
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/OnboardingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/OnboardingTest.kt
new file mode 100644
index 0000000000..4eead4f63c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/OnboardingTest.kt
@@ -0,0 +1,73 @@
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithLauncherIntent
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+
+class OnboardingTest : TestSetup() {
+
+ @get:Rule
+ val activityTestRule =
+ AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(launchActivity = false),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122321
+ @Test
+ fun verifyFirstOnboardingCardItemsTest() {
+ runWithLauncherIntent(activityTestRule) {
+ homeScreen {
+ verifyFirstOnboardingCard(activityTestRule)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122334
+ @SmokeTest
+ @Test
+ fun verifyFirstOnboardingCardItemsFunctionalityTest() {
+ runWithLauncherIntent(activityTestRule) {
+ homeScreen {
+ clickDefaultCardNotNowOnboardingButton(activityTestRule)
+ verifySecondOnboardingCard(activityTestRule)
+ swipeSecondOnboardingCardToRight()
+ }.clickSetAsDefaultBrowserOnboardingButton(activityTestRule) {
+ verifyAndroidDefaultAppsMenuAppears()
+ }.goBackToOnboardingScreen {
+ verifySecondOnboardingCard(activityTestRule)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122343
+ @Test
+ fun verifySecondOnboardingCardItemsTest() {
+ runWithLauncherIntent(activityTestRule) {
+ homeScreen {
+ clickDefaultCardNotNowOnboardingButton(activityTestRule)
+ verifySecondOnboardingCard(activityTestRule)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122344
+ @SmokeTest
+ @Test
+ fun verifyThirdOnboardingCardSignInFunctionalityTest() {
+ runWithLauncherIntent(activityTestRule) {
+ homeScreen {
+ clickDefaultCardNotNowOnboardingButton(activityTestRule)
+ verifySecondOnboardingCard(activityTestRule)
+ clickAddSearchWidgetNotNowOnboardingButton(activityTestRule)
+ verifyThirdOnboardingCard(activityTestRule)
+ }.clickSignInOnboardingButton(activityTestRule) {
+ verifyTurnOnSyncMenu()
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt
new file mode 100644
index 0000000000..482e740b58
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
+import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_DOCS
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class PDFViewerTest : TestSetup() {
+ private val downloadTestPage =
+ "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
+ private val pdfFileName = "washington.pdf"
+ private val pdfFileURL = "storage.googleapis.com/mobile_test_assets/public/washington.pdf"
+ private val pdfFileContent = "Washington Crossing the Delaware"
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2048140
+ @SmokeTest
+ @Test
+ fun verifyPDFFileIsOpenedInTheSameTabTest() {
+ val genericURL =
+ getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemContainingText("PDF form file"))
+ verifyPageContent("Washington Crossing the Delaware")
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2145448
+ // Download PDF file using the download toolbar button
+ @Test
+ fun verifyPDFViewerDownloadButtonTest() {
+ val genericURL = getGenericAsset(mockWebServer, 3)
+ val downloadFile = "pdfForm.pdf"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ }.clickDownloadPDFButton {
+ verifyDownloadedFileName(downloadFile)
+ }.clickOpen("application/pdf") {
+ assertExternalAppOpens(GOOGLE_DOCS)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2283305
+ @Test
+ fun pdfFindInPageTest() {
+ val genericURL = getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(MatcherHelper.itemWithText("PDF form file"))
+ }.openThreeDotMenu {
+ verifyThreeDotMenuExists()
+ verifyFindInPageButton()
+ }.openFindInPage {
+ verifyFindInPageNextButton()
+ verifyFindInPagePrevButton()
+ verifyFindInPageCloseButton()
+ enterFindInPageQuery("l")
+ verifyFindInPageResult("1/2")
+ clickFindInPageNextButton()
+ verifyFindInPageResult("2/2")
+ clickFindInPagePrevButton()
+ verifyFindInPageResult("1/2")
+ }.closeFindInPageWithCloseButton {
+ verifyFindInPageBar(false)
+ }.openThreeDotMenu {
+ }.openFindInPage {
+ enterFindInPageQuery("p")
+ verifyFindInPageResult("1/1")
+ }.closeFindInPageWithBackButton {
+ verifyFindInPageBar(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2284297
+ @Test
+ fun addPDFToHomeScreenTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
+ clickPageObject(MatcherHelper.itemContainingText(pdfFileName))
+ verifyUrl(pdfFileURL)
+ verifyPageContent(pdfFileContent)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.openAddToHomeScreen {
+ verifyShortcutTextFieldTitle(pdfFileName)
+ clickAddShortcutButton()
+ clickAddAutomaticallyButton()
+ }.openHomeScreenShortcut(pdfFileName) {
+ verifyUrl(pdfFileURL)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PocketTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PocketTest.kt
new file mode 100644
index 0000000000..674924c954
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PocketTest.kt
@@ -0,0 +1,127 @@
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+
+/**
+ * Tests for verifying the presence of the Pocket section and its elements
+ */
+
+class PocketTest : TestSetup() {
+ private lateinit var firstPocketStoryPublisher: String
+
+ @get:Rule(order = 0)
+ val activityTestRule =
+ AndroidComposeTestRule(HomeActivityTestRule.withDefaultSettingsOverrides()) { it.activity }
+
+ @Rule(order = 1)
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ // Workaround to make sure the Pocket articles are populated before starting the tests.
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.goBack {}
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2252509
+ @Test
+ fun verifyPocketSectionTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isRecentTabsFeatureEnabled = false
+ it.isRecentlyVisitedFeatureEnabled = false
+ }
+
+ homeScreen {
+ verifyThoughtProvokingStories(true)
+ scrollToPocketProvokingStories()
+ verifyPocketRecommendedStoriesItems()
+ // Sponsored Pocket stories are only advertised for a limited time.
+ // See also known issue https://bugzilla.mozilla.org/show_bug.cgi?id=1828629
+ // verifyPocketSponsoredStoriesItems(2, 8)
+ verifyDiscoverMoreStoriesButton()
+ verifyStoriesByTopic(true)
+ verifyPoweredByPocket()
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ clickPocketButton()
+ }.goBackToHomeScreen {
+ verifyThoughtProvokingStories(false)
+ verifyStoriesByTopic(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2252513
+ @Test
+ fun openPocketStoryItemTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isRecentTabsFeatureEnabled = false
+ it.isRecentlyVisitedFeatureEnabled = false
+ }
+
+ homeScreen {
+ verifyThoughtProvokingStories(true)
+ scrollToPocketProvokingStories()
+ firstPocketStoryPublisher = getProvokingStoryPublisher(1)
+ }.clickPocketStoryItem(firstPocketStoryPublisher, 1) {
+ verifyUrl(Constants.POCKET_RECOMMENDED_STORIES_UTM_PARAM)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2252514
+ @Test
+ fun pocketDiscoverMoreButtonTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isRecentTabsFeatureEnabled = false
+ it.isRecentlyVisitedFeatureEnabled = false
+ }
+
+ homeScreen {
+ scrollToPocketProvokingStories()
+ verifyDiscoverMoreStoriesButton()
+ }.clickPocketDiscoverMoreButton {
+ verifyUrl("getpocket.com/explore")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2252515
+ @Test
+ fun selectPocketStoriesByTopicTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isRecentTabsFeatureEnabled = false
+ it.isRecentlyVisitedFeatureEnabled = false
+ }
+
+ homeScreen {
+ verifyStoriesByTopicItemState(activityTestRule, false, 1)
+ clickStoriesByTopicItem(activityTestRule, 1)
+ verifyStoriesByTopicItemState(activityTestRule, true, 1)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2252516
+ @Test
+ fun pocketLearnMoreButtonTest() {
+ activityTestRule.activityRule.applySettingsExceptions {
+ it.isRecentTabsFeatureEnabled = false
+ it.isRecentlyVisitedFeatureEnabled = false
+ }
+
+ homeScreen {
+ verifyPoweredByPocket()
+ }.clickPocketLearnMoreLink(activityTestRule) {
+ verifyUrl("mozilla.org/en-US/firefox/pocket")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PwaTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PwaTest.kt
new file mode 100644
index 0000000000..d9ac033a9c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/PwaTest.kt
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.customTabScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.pwaScreen
+
+class PwaTest : TestSetup() {
+ /* Updated externalLinks.html to v2.0,
+ changed the hypertext reference to mozilla-mobile.github.io/testapp/downloads for "External link"
+ */
+ private val externalLinksPWAPage = "https://mozilla-mobile.github.io/testapp/v2.0/externalLinks.html"
+ private val emailLink = "mailto://example@example.com"
+ private val phoneLink = "tel://1234567890"
+ private val shortcutTitle = "TEST_APP"
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/845695
+ @Test
+ fun externalLinkPWATest() {
+ val externalLinkURL = "https://mozilla-mobile.github.io/testapp/downloads"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPWAPage.toUri()) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.clickInstall {
+ clickAddAutomaticallyButton()
+ }.openHomeScreenShortcut(shortcutTitle) {
+ clickPageObject(itemContainingText("External link"))
+ }
+
+ customTabScreen {
+ verifyCustomTabToolbarTitle(externalLinkURL)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/845694
+ @Test
+ fun appLikeExperiencePWATest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPWAPage.toUri()) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.clickInstall {
+ clickAddAutomaticallyButton()
+ }.openHomeScreenShortcut(shortcutTitle) {
+ }
+
+ pwaScreen {
+ verifyCustomTabToolbarIsNotDisplayed()
+ verifyPwaActivityInCurrentTask()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/834200
+ @SmokeTest
+ @Test
+ fun installPWAFromTheMainMenuTest() {
+ val pwaPage = "https://mozilla-mobile.github.io/testapp/"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(pwaPage.toUri()) {
+ }.openThreeDotMenu {
+ }.clickInstall {
+ clickAddAutomaticallyButton()
+ }.openHomeScreenShortcut("TEST_APP") {
+ mDevice.waitForIdle()
+ verifyNavURLBarHidden()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt
new file mode 100644
index 0000000000..8e138aa8be
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.view.View
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic functionality of content context menus
+ *
+ * - Verifies Reader View entry and detection when available UI and functionality
+ * - Verifies Reader View exit UI and functionality
+ * - Verifies Reader View appearance controls UI and functionality
+ *
+ */
+
+class ReaderViewTest : TestSetup() {
+ private val estimatedReadingTime = "1 - 2 minutes"
+
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ /**
+ * Verify that Reader View capable pages
+ *
+ * - Show the toggle button in the navigation bar
+ *
+ */
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/250592
+ @Test
+ fun verifyReaderModePageDetectionTest() {
+ val readerViewPage =
+ TestAssetHelper.getLoremIpsumAsset(mockWebServer)
+ val genericPage =
+ TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(readerViewPage.url) {
+ mDevice.waitForIdle()
+ }
+
+ registerAndCleanupIdlingResources(
+ ViewVisibilityIdlingResource(
+ activityIntentTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
+ View.VISIBLE,
+ ),
+ ) {}
+
+ navigationToolbar {
+ verifyReaderViewDetected(true)
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ }
+ navigationToolbar {
+ verifyReaderViewDetected(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/250585
+ @SmokeTest
+ @Test
+ fun verifyReaderModeControlsTest() {
+ val readerViewPage =
+ TestAssetHelper.getLoremIpsumAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(readerViewPage.url) {
+ mDevice.waitForIdle()
+ }
+
+ registerAndCleanupIdlingResources(
+ ViewVisibilityIdlingResource(
+ activityIntentTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
+ View.VISIBLE,
+ ),
+ ) {}
+
+ navigationToolbar {
+ verifyReaderViewDetected(true)
+ toggleReaderView()
+ mDevice.waitForIdle()
+ }
+
+ browserScreen {
+ verifyPageContent(estimatedReadingTime)
+ }.openThreeDotMenu {
+ verifyReaderViewAppearance(true)
+ }.openReaderViewAppearance {
+ verifyAppearanceFontGroup(true)
+ verifyAppearanceFontSansSerif(true)
+ verifyAppearanceFontSerif(true)
+ verifyAppearanceFontIncrease(true)
+ verifyAppearanceFontDecrease(true)
+ verifyAppearanceFontSize(3)
+ verifyAppearanceColorGroup(true)
+ verifyAppearanceColorDark(true)
+ verifyAppearanceColorLight(true)
+ verifyAppearanceColorSepia(true)
+ }.toggleSansSerif {
+ verifyAppearanceFontIsActive("SANSSERIF")
+ }.toggleSerif {
+ verifyAppearanceFontIsActive("SERIF")
+ }.toggleFontSizeIncrease {
+ verifyAppearanceFontSize(4)
+ }.toggleFontSizeIncrease {
+ verifyAppearanceFontSize(5)
+ }.toggleFontSizeIncrease {
+ verifyAppearanceFontSize(6)
+ }.toggleFontSizeDecrease {
+ verifyAppearanceFontSize(5)
+ }.toggleFontSizeDecrease {
+ verifyAppearanceFontSize(4)
+ }.toggleFontSizeDecrease {
+ verifyAppearanceFontSize(3)
+ }.toggleColorSchemeChangeDark {
+ verifyAppearanceColorSchemeChange("DARK")
+ }.toggleColorSchemeChangeSepia {
+ verifyAppearanceColorSchemeChange("SEPIA")
+ }.toggleColorSchemeChangeLight {
+ verifyAppearanceColorSchemeChange("LIGHT")
+ }.closeAppearanceMenu {
+ }
+ navigationToolbar {
+ toggleReaderView()
+ mDevice.waitForIdle()
+ verifyReaderViewDetected(true)
+ }.openThreeDotMenu {
+ verifyReaderViewAppearance(false)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt
new file mode 100644
index 0000000000..044696b36c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying basic functionality of recently closed tabs history
+ *
+ */
+class RecentlyClosedTabsTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(
+ tabsTrayRewriteEnabled = true,
+ ),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1065414
+ // Verifies that a recently closed item is properly opened
+ @SmokeTest
+ @Test
+ fun openRecentlyClosedItemTest() {
+ val website = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(website.url) {
+ mDevice.waitForIdle()
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTab()
+ }
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openRecentlyClosedTabs {
+ waitForListToExist()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
+ ) {
+ verifyRecentlyClosedTabsMenuView()
+ verifyRecentlyClosedTabsPageTitle("Test_Page_1")
+ verifyRecentlyClosedTabsUrl(website.url)
+ }
+ }.clickRecentlyClosedItem("Test_Page_1") {
+ verifyUrl(website.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2195812
+ // Verifies that tapping the "x" button removes a recently closed item from the list
+ @SmokeTest
+ @Test
+ fun deleteRecentlyClosedTabsItemTest() {
+ val website = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(website.url) {
+ mDevice.waitForIdle()
+ }.openComposeTabDrawer(activityTestRule) {
+ closeTab()
+ }
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openRecentlyClosedTabs {
+ waitForListToExist()
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
+ ) {
+ verifyRecentlyClosedTabsMenuView()
+ }
+ clickDeleteRecentlyClosedTabs()
+ verifyEmptyRecentlyClosedTabsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1605515
+ @Test
+ fun openMultipleRecentlyClosedTabsTest() {
+ val firstPage = getGenericAsset(mockWebServer, 1)
+ val secondPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondPage.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openRecentlyClosedTabs {
+ waitForListToExist()
+ longTapSelectItem(firstPage.url)
+ longTapSelectItem(secondPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }.clickOpenInNewTab(activityTestRule) {
+ // URL verification to be removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1839179 is fixed.
+ browserScreen {
+ verifyPageContent(secondPage.content)
+ verifyUrl(secondPage.url.toString())
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyNormalBrowsingButtonIsSelected(true)
+ verifyExistingOpenTabs(firstPage.title, secondPage.title)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2198690
+ @Test
+ fun openRecentlyClosedTabsInPrivateBrowsingTest() {
+ val firstPage = getGenericAsset(mockWebServer, 1)
+ val secondPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondPage.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openRecentlyClosedTabs {
+ waitForListToExist()
+ longTapSelectItem(firstPage.url)
+ longTapSelectItem(secondPage.url)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }.clickOpenInPrivateTab(activityTestRule) {
+ // URL verification to be removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1839179 is fixed.
+ browserScreen {
+ verifyPageContent(secondPage.content)
+ verifyUrl(secondPage.url.toString())
+ }.openComposeTabDrawer(activityTestRule) {
+ verifyPrivateBrowsingButtonIsSelected(true)
+ verifyExistingOpenTabs(firstPage.title, secondPage.title)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1605514
+ @Test
+ fun shareMultipleRecentlyClosedTabsTest() {
+ val firstPage = getGenericAsset(mockWebServer, 1)
+ val secondPage = getGenericAsset(mockWebServer, 2)
+ val sharingApp = "Gmail"
+ val urlString = "${firstPage.url}\n\n${secondPage.url}"
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondPage.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openRecentlyClosedTabs {
+ waitForListToExist()
+ longTapSelectItem(firstPage.url)
+ longTapSelectItem(secondPage.url)
+ }.clickShare {
+ verifyShareTabsOverlay(firstPage.title, secondPage.title)
+ verifySharingWithSelectedApp(sharingApp, urlString, "${firstPage.title}, ${secondPage.title}")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1065438
+ @Test
+ fun closedPrivateTabsAreNotSavedInRecentlyClosedTabsTest() {
+ val firstPage = getGenericAsset(mockWebServer, 1)
+ val secondPage = getGenericAsset(mockWebServer, 2)
+
+ homeScreen {}.togglePrivateBrowsingMode()
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondPage.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openRecentlyClosedTabs {
+ verifyEmptyRecentlyClosedTabsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1065439
+ @Test
+ fun deletingBrowserHistoryClearsRecentlyClosedTabsListTest() {
+ val firstPage = getGenericAsset(mockWebServer, 1)
+ val secondPage = getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstPage.url) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openNewTab {
+ }.submitQuery(secondPage.url.toString()) {
+ waitForPageToLoad()
+ }.openComposeTabDrawer(activityTestRule) {
+ }.openThreeDotMenu {
+ }.closeAllTabs {
+ }.openThreeDotMenu {
+ }.openHistory {
+ }.openRecentlyClosedTabs {
+ waitForListToExist()
+ }.goBackToHistoryMenu {
+ clickDeleteAllHistoryButton()
+ selectEverythingOption()
+ confirmDeleteAllHistory()
+ verifyEmptyHistoryView()
+ }.openRecentlyClosedTabs {
+ verifyEmptyRecentlyClosedTabsList()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt
new file mode 100644
index 0000000000..ede7b06b5d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt
@@ -0,0 +1,815 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.content.Context
+import android.hardware.camera2.CameraManager
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.core.net.toUri
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import androidx.test.espresso.Espresso.pressBack
+import mozilla.components.concept.engine.utils.EngineReleaseChannel
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens
+import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission
+import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
+import org.mozilla.fenix.helpers.AppAndSystemHelper.verifyKeyboardVisibility
+import org.mozilla.fenix.helpers.Constants.PackageName.ANDROID_SETTINGS
+import org.mozilla.fenix.helpers.Constants.searchEngineCodes
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createBookmarkItem
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createHistoryItem
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createTabItem
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.setCustomSearchEngine
+import org.mozilla.fenix.helpers.SearchDispatcher
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickContextMenuItem
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.longClickPageObject
+import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.searchScreen
+import java.util.Locale
+
+/**
+ * Tests for verifying the search fragment
+ *
+ * Including:
+ * - Verify the toolbar, awesomebar, and shortcut bar are displayed
+ * - Select shortcut button
+ * - Select scan button
+ *
+ */
+
+class SearchTest : TestSetup() {
+ private lateinit var searchMockServer: MockWebServer
+ private var queryString = "firefox"
+ private val generalEnginesList = listOf("DuckDuckGo", "Google", "Bing")
+ private val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
+
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityTestRule(
+ skipOnboarding = true,
+ isPocketEnabled = false,
+ isJumpBackInCFREnabled = false,
+ isRecentTabsFeatureEnabled = false,
+ isTCPCFREnabled = false,
+ isWallpaperOnboardingEnabled = false,
+ tabsTrayRewriteEnabled = false,
+ ),
+ ) { it.activity }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ searchMockServer = MockWebServer().apply {
+ dispatcher = SearchDispatcher()
+ start()
+ }
+ }
+
+ @After
+ override fun tearDown() {
+ searchMockServer.shutdown()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189
+ @Test
+ fun verifySearchBarItemsTest() {
+ navigationToolbar {
+ verifyDefaultSearchEngine("Google")
+ verifySearchBarPlaceholder("Search or enter address")
+ }.clickUrlbar {
+ verifyKeyboardVisibility(isExpectedToBeVisible = true)
+ verifyScanButtonVisibility(visible = true)
+ verifyVoiceSearchButtonVisibility(enabled = true)
+ verifySearchBarPlaceholder("Search or enter address")
+ typeSearch("mozilla ")
+ verifyScanButtonVisibility(visible = false)
+ verifyVoiceSearchButtonVisibility(enabled = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154190
+ @Test
+ fun verifySearchSelectorMenuItemsTest() {
+ homeScreen {
+ }.openSearch {
+ verifySearchView()
+ verifySearchToolbar(isDisplayed = true)
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(
+ "DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay",
+ "Bookmarks", "Tabs", "History", "Search settings",
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154194
+ @Test
+ fun verifySearchPlaceholderForGeneralDefaultSearchEnginesTest() {
+ generalEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ }.clickSearchEngineSettings {
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine(it)
+ exitMenu()
+ }
+ navigationToolbar {
+ verifySearchBarPlaceholder("Search or enter address")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154195
+ @Test
+ fun verifySearchPlaceholderForNotDefaultGeneralSearchEnginesTest() {
+ val generalEnginesList = listOf("DuckDuckGo", "Bing")
+
+ generalEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifySearchBarPlaceholder("Search the web")
+ }.dismissSearchBar {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154196
+ @Test
+ fun verifySearchPlaceholderForTopicSpecificSearchEnginesTest() {
+ val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay")
+
+ topicEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifySearchBarPlaceholder("Enter search terms")
+ }.dismissSearchBar {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1059459
+ @SmokeTest
+ @Test
+ fun verifyQRScanningCameraAccessDialogTest() {
+ val cameraManager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ homeScreen {
+ }.openSearch {
+ clickScanButton()
+ denyPermission()
+ clickScanButton()
+ clickDismissPermissionRequiredDialog()
+ }
+ homeScreen {
+ }.openSearch {
+ clickScanButton()
+ clickGoToPermissionsSettings()
+ assertNativeAppOpens(ANDROID_SETTINGS)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235397
+ @SmokeTest
+ @Test
+ fun scanQRCodeToOpenAWebpageTest() {
+ val cameraManager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ homeScreen {
+ }.openSearch {
+ clickScanButton()
+ grantSystemPermission()
+ verifyScannerOpen()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154191
+ @Test
+ fun verifyScanButtonAvailableOnlyForGeneralSearchEnginesTest() {
+ generalEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifyScanButtonVisibility(visible = true)
+ }.dismissSearchBar {}
+ }
+
+ topicEnginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(it)
+ verifyScanButtonVisibility(visible = false)
+ }.dismissSearchBar {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235395
+ // Verifies a temporary change of search engine from the Search shortcut menu
+ @SmokeTest
+ @Test
+ fun searchEnginesCanBeChangedTemporarilyFromSearchSelectorMenuTest() {
+ val enginesList = listOf("DuckDuckGo", "Google", "Amazon.com", "Wikipedia", "Bing", "eBay")
+
+ enginesList.forEach {
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(it)
+ selectTemporarySearchMethod(it)
+ verifySearchEngineIcon(it)
+ }.submitQuery("mozilla ") {
+ verifyUrl("mozilla")
+ }.goToHomescreen {}
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233589
+ @Test
+ fun defaultSearchEnginesCanBeSetFromSearchSelectorMenuTest() {
+ searchScreen {
+ clickSearchSelectorButton()
+ }.clickSearchEngineSettings {
+ verifyToolbarText("Search")
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine("DuckDuckGo")
+ exitMenu()
+ }
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ verifyUrl(queryString)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522918
+ @Test
+ fun verifyClearSearchButtonTest() {
+ homeScreen {
+ }.openSearch {
+ typeSearch(queryString)
+ clickClearButton()
+ verifySearchBarPlaceholder("Search or enter address")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1623441
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @SmokeTest
+ @Test
+ fun searchResultsOpenedInNewTabsGenerateSearchGroupsTest() {
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ pressBack()
+ longClickPageObject(itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592229
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun verifyAPageIsAddedToASearchGroupOnlyOnceTest() {
+ val firstPageUrl = getGenericAsset(searchMockServer, 1).url
+ val secondPageUrl = getGenericAsset(searchMockServer, 2).url
+ val originPageUrl =
+ "http://localhost:${searchMockServer.port}/pages/searchResults.html?search=test%20search".toUri()
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ pressBack()
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ pressBack()
+ longClickPageObject(itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ pressBack()
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ verifyTestPageUrl(firstPageUrl)
+ verifyTestPageUrl(secondPageUrl)
+ verifyTestPageUrl(originPageUrl)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1591782
+ @Ignore("Failing due to known bug, see https://github.com/mozilla-mobile/fenix/issues/23818")
+ @Test
+ fun searchGroupIsGeneratedWhenNavigatingInTheSameTabTest() {
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ clickPageObject(itemContainingText("Link 1"))
+ waitForPageToLoad()
+ pressBack()
+ clickPageObject(itemContainingText("Link 2"))
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1591781
+ @SmokeTest
+ @Test
+ fun searchGroupIsNotGeneratedForLinksOpenedInPrivateTabsTest() {
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in private tab")
+ longClickPageObject(itemWithText("Link 2"))
+ clickContextMenuItem("Open link in private tab")
+ }.openTabDrawer {
+ }.toggleToPrivateTabs {
+ }.openTabWithIndex(0) {
+ }.openTabDrawer {
+ }.openTabWithIndex(1) {
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ togglePrivateBrowsingModeOnOff()
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 3)
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyHistoryItemExists(shouldExist = false, item = "3 sites")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592269
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @SmokeTest
+ @Test
+ fun deleteIndividualHistoryItemsFromSearchGroupTest() {
+ val firstPageUrl = getGenericAsset(searchMockServer, 1).url
+ val secondPageUrl = getGenericAsset(searchMockServer, 2).url
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ mDevice.pressBack()
+ longClickPageObject(itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ clickDeleteHistoryButton(firstPageUrl.toString())
+ longTapSelectItem(secondPageUrl)
+ multipleSelectionToolbar {
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ clickMultiSelectionDelete()
+ }
+ exitMenu()
+ }
+ homeScreen {
+ // checking that the group is removed when only 1 item is left
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, searchTerm = queryString, groupSize = 1)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592242
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun deleteSearchGroupFromHomeScreenTest() {
+ val firstPageUrl = getGenericAsset(searchMockServer, 1).url
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ mDevice.pressBack()
+ longClickPageObject(itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ clickDeleteAllHistoryButton()
+ confirmDeleteAllHistory()
+ verifySnackBarText("Group deleted")
+ verifyHistoryItemExists(shouldExist = false, firstPageUrl.toString())
+ }.goBack {}
+ homeScreen {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifySearchGroupDisplayed(shouldBeDisplayed = false, queryString, groupSize = 3)
+ verifyEmptyHistoryView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592235
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun openAPageFromHomeScreenSearchGroupTest() {
+ val firstPageUrl = getGenericAsset(searchMockServer, 1).url
+ val secondPageUrl = getGenericAsset(searchMockServer, 2).url
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ mDevice.pressBack()
+ longClickPageObject(itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ }.openWebsite(firstPageUrl) {
+ verifyUrl(firstPageUrl.toString())
+ }.goToHomescreen {
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ longTapSelectItem(firstPageUrl)
+ longTapSelectItem(secondPageUrl)
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ }
+
+ multipleSelectionToolbar {
+ }.clickOpenNewTab {
+ verifyNormalModeSelected()
+ }.closeTabDrawer {}
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ multipleSelectionToolbar {
+ }.clickOpenPrivateTab {
+ verifyPrivateModeSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592238
+ @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
+ @Test
+ fun shareAPageFromHomeScreenSearchGroupTest() {
+ val firstPageUrl = getGenericAsset(searchMockServer, 1).url
+ // setting our custom mockWebServer search URL
+ val searchEngineName = "TestSearchEngine"
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+
+ // Performs a search and opens 2 dummy search results links to create a search group
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ longClickPageObject(itemWithText("Link 1"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ mDevice.pressBack()
+ longClickPageObject(itemWithText("Link 2"))
+ clickContextMenuItem("Open link in new tab")
+ clickSnackbarButton("SWITCH")
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openTabsListThreeDotMenu {
+ }.closeAllTabs {
+ verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed = true, searchTerm = queryString, groupSize = 3)
+ }.openRecentlyVisitedSearchGroupHistoryList(queryString) {
+ longTapSelectItem(firstPageUrl)
+ }
+
+ multipleSelectionToolbar {
+ clickShareHistoryButton()
+ verifyShareOverlay()
+ verifyShareTabFavicon()
+ verifyShareTabTitle()
+ verifyShareTabUrl()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232633
+ // Default search code for Google-US
+ @Test
+ fun defaultSearchCodeGoogleUS() {
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.openHistory {
+ // Full URL no longer visible in the nav bar, so we'll check the history record
+ // A search group is sometimes created when searching with Google (probably redirects)
+ try {
+ verifyHistoryItemExists(shouldExist = true, searchEngineCodes["Google"]!!)
+ } catch (e: AssertionError) {
+ openSearchGroup(queryString)
+ verifyHistoryItemExists(shouldExist = true, searchEngineCodes["Google"]!!)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232637
+ // Default search code for Bing-US
+ @Test
+ fun defaultSearchCodeBingUS() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine("Bing")
+ exitMenu()
+ }
+
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.openHistory {
+ // Full URL no longer visible in the nav bar, so we'll check the history record
+ // A search group is sometimes created when searching with Bing (probably redirects)
+ try {
+ verifyHistoryItemExists(shouldExist = true, searchEngineCodes["Bing"]!!)
+ } catch (e: AssertionError) {
+ openSearchGroup(queryString)
+ verifyHistoryItemExists(shouldExist = true, searchEngineCodes["Bing"]!!)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232638
+ // Default search code for DuckDuckGo-US
+ @Test
+ fun defaultSearchCodeDuckDuckGoUS() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine("DuckDuckGo")
+ exitMenu()
+ }
+ homeScreen {
+ }.openSearch {
+ }.submitQuery(queryString) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ }.openHistory {
+ // Full URL no longer visible in the nav bar, so we'll check the history record
+ // A search group is sometimes created when searching with DuckDuckGo
+ try {
+ verifyHistoryItemExists(shouldExist = true, item = searchEngineCodes["DuckDuckGo"]!!)
+ } catch (e: AssertionError) {
+ openSearchGroup(queryString)
+ verifyHistoryItemExists(shouldExist = true, item = searchEngineCodes["DuckDuckGo"]!!)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154215
+ @SmokeTest
+ @Test
+ fun verifyHistorySearchWithBrowsingHistoryTest() {
+ val firstPageUrl = getGenericAsset(searchMockServer, 1)
+ val secondPageUrl = getGenericAsset(searchMockServer, 2)
+
+ createHistoryItem(firstPageUrl.url.toString())
+ createHistoryItem(secondPageUrl.url.toString())
+
+ navigationToolbar {
+ }.clickUrlbar {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(searchEngineName = "History")
+ typeSearch(searchTerm = "Mozilla")
+ verifySuggestionsAreNotDisplayed(rule = activityTestRule, "Mozilla")
+ clickClearButton()
+ typeSearch(searchTerm = "generic")
+ verifyTypedToolbarText("generic")
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ firstPageUrl.url.toString(),
+ secondPageUrl.url.toString(),
+ ),
+ searchTerm = "generic",
+ )
+ }.clickSearchSuggestion(firstPageUrl.url.toString()) {
+ verifyUrl(firstPageUrl.url.toString())
+ }
+ }
+
+ @SmokeTest
+ @Test
+ fun verifySearchTabsWithOpenTabsTest() {
+ runWithCondition(
+ // This test will run only on Beta and RC builds
+ // The new composable tabs tray is only available in Nightly and Debug.
+ activityTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.RELEASE ||
+ activityTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
+ ) {
+ val firstPageUrl = getGenericAsset(searchMockServer, 1)
+ val secondPageUrl = getGenericAsset(searchMockServer, 2)
+
+ createTabItem(firstPageUrl.url.toString())
+ createTabItem(secondPageUrl.url.toString())
+
+ navigationToolbar {
+ }.clickUrlbar {
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod(searchEngineName = "Tabs")
+ typeSearch(searchTerm = "Mozilla")
+ verifySuggestionsAreNotDisplayed(rule = activityTestRule, "Mozilla")
+ clickClearButton()
+ typeSearch(searchTerm = "generic")
+ verifyTypedToolbarText("generic")
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "Firefox Suggest",
+ firstPageUrl.url.toString(),
+ secondPageUrl.url.toString(),
+ ),
+ searchTerm = "generic",
+ )
+ }.clickSearchSuggestion(firstPageUrl.url.toString()) {
+ verifyTabCounter("2")
+ }.openTabDrawer {
+ verifyOpenTabsOrder(position = 1, title = firstPageUrl.url.toString())
+ verifyOpenTabsOrder(position = 2, title = secondPageUrl.url.toString())
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2230212
+ @SmokeTest
+ @Test
+ fun searchHistoryNotRememberedInPrivateBrowsingTest() {
+ appContext.settings().shouldShowSearchSuggestionsInPrivate = true
+
+ val firstPageUrl = getGenericAsset(searchMockServer, 1)
+ val searchEngineName = "TestSearchEngine"
+
+ setCustomSearchEngine(searchMockServer, searchEngineName)
+ createBookmarkItem(firstPageUrl.url.toString(), firstPageUrl.title, 1u)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.clickUrlbar {
+ }.submitQuery("test page 1") {
+ }.goToHomescreen {
+ }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.clickUrlbar {
+ }.submitQuery("test page 2") {
+ }.openNavigationToolbar {
+ }.clickUrlbar {
+ typeSearch(searchTerm = "test page")
+ verifySearchEngineSuggestionResults(
+ rule = activityTestRule,
+ searchSuggestions = arrayOf(
+ "TestSearchEngine search",
+ "test page 1",
+ "Firefox Suggest",
+ firstPageUrl.url.toString(),
+ ),
+ searchTerm = "test page 1",
+ )
+ // 2 search engine suggestions and 2 browser suggestions (1 history, 1 bookmark)
+ verifySearchSuggestionsCount(activityTestRule, numberOfSuggestions = 4, searchTerm = "test page")
+ verifySuggestionsAreNotDisplayed(
+ activityTestRule,
+ searchSuggestions = arrayOf(
+ "test page 2",
+ ),
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232631
+ // Expected for app language set to Arabic
+ @Test
+ fun verifySearchEnginesFunctionalityUsingRTLLocaleTest() {
+ val arabicLocale = Locale("ar", "AR")
+
+ runWithSystemLocaleChanged(arabicLocale, activityTestRule.activityRule) {
+ homeScreen {
+ }.openSearch {
+ verifyTranslatedFocusedNavigationToolbar("ابحث أو أدخِل عنوانا")
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(
+ "Google",
+ "Bing",
+ "Amazon.com",
+ "DuckDuckGo",
+ "ويكيبيديا (ar)",
+ )
+ selectTemporarySearchMethod("ويكيبيديا (ar)")
+ }.submitQuery("firefox") {
+ verifyUrl("firefox")
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt
new file mode 100644
index 0000000000..3e37fb5e3b
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt
@@ -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 http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.test.uiautomator.UiSelector
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickRateButtonGooglePlay
+import org.mozilla.fenix.ui.robots.homeScreen
+
+/**
+ * Tests for verifying the main three dot menu options
+ *
+ */
+
+class SettingsAboutTest : TestSetup() {
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule()
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // Walks through the About settings menu to ensure all items are present
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092700
+ @Test
+ fun verifyAboutSettingsItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyAboutHeading()
+ verifyRateOnGooglePlay()
+ verifyAboutFirefoxPreview()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/246966
+ @Test
+ fun verifyRateOnGooglePlayButton() {
+ activityIntentTestRule.applySettingsExceptions {
+ it.isTCPCFREnabled = false
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ clickRateButtonGooglePlay()
+ verifyGooglePlayRedirect()
+ // press back to return to the app, or accept ToS if still visible
+ mDevice.pressBack()
+ dismissGooglePlayToS()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/246961
+ @Test
+ fun verifyAboutFirefoxMenuItems() {
+ activityIntentTestRule.applySettingsExceptions {
+ it.isJumpBackInCFREnabled = false
+ it.isTCPCFREnabled = false
+ }
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAboutFirefoxPreview {
+ verifyAboutFirefoxPreviewInfo()
+ }
+ }
+}
+
+private fun dismissGooglePlayToS() {
+ if (mDevice.findObject(UiSelector().textContains("Terms of Service")).exists()) {
+ mDevice.findObject(UiSelector().textContains("ACCEPT")).click()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt
new file mode 100644
index 0000000000..695d0af3c0
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper.getEnhancedTrackingProtectionAsset
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.addonsMenu
+import org.mozilla.fenix.ui.robots.homeScreen
+
+/**
+ * Tests for verifying the functionality of installing or removing addons
+ *
+ */
+class SettingsAddonsTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/875780
+ // Walks through settings add-ons menu to ensure all items are present
+ @Test
+ fun verifyAddonsListItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyAdvancedHeading()
+ verifyAddons()
+ }.openAddonsManagerMenu {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.add_ons_list), 1),
+ ) {
+ verifyAddonsItems()
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/875781
+ // Installs an add-on from the Add-ons menu and verifies the prompts
+ @Test
+ fun installAddonFromMainMenuTest() {
+ val addonName = "uBlock Origin"
+
+ homeScreen {}
+ .openThreeDotMenu {}
+ .openAddonsManagerMenu {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(
+ activityTestRule.activity.findViewById(R.id.add_ons_list),
+ 1,
+ ),
+ ) {
+ clickInstallAddon(addonName)
+ }
+ verifyAddonDownloadOverlay()
+ verifyAddonPermissionPrompt(addonName)
+ cancelInstallAddon()
+ clickInstallAddon(addonName)
+ acceptPermissionToInstallAddon()
+ verifyAddonInstallCompleted(addonName, activityTestRule)
+ verifyAddonInstallCompletedPrompt(addonName)
+ closeAddonInstallCompletePrompt()
+ verifyAddonIsInstalled(addonName)
+ verifyEnabledTitleDisplayed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561597
+ // Installs an addon, then uninstalls it
+ @Test
+ fun verifyAddonsCanBeUninstalledTest() {
+ val addonName = "uBlock Origin"
+
+ addonsMenu {
+ installAddon(addonName, activityTestRule)
+ closeAddonInstallCompletePrompt()
+ }.openDetailedMenuForAddon(addonName) {
+ }.removeAddon(activityTestRule) {
+ verifySnackBarText("Successfully uninstalled $addonName")
+ waitUntilSnackbarGone()
+ }.goBack {
+ }.openThreeDotMenu {
+ }.openAddonsManagerMenu {
+ verifyAddonCanBeInstalled(addonName)
+ }
+ }
+
+ // TODO: Harden to dynamically install addons from position
+ // in list of detected addons on screen instead of hard-coded values.
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561600
+ // Installs 2 add-on and checks that the app doesn't crash while navigating the app
+ @SmokeTest
+ @Test
+ fun noCrashWithAddonInstalledTest() {
+ // setting ETP to Strict mode to test it works with add-ons
+ activityTestRule.activity.settings().setStrictETP()
+
+ val uBlockAddon = "uBlock Origin"
+ val darkReaderAddon = "Dark Reader"
+ val trackingProtectionPage = getEnhancedTrackingProtectionAsset(mockWebServer)
+
+ addonsMenu {
+ installAddon(uBlockAddon, activityTestRule)
+ closeAddonInstallCompletePrompt()
+ installAddon(darkReaderAddon, activityTestRule)
+ closeAddonInstallCompletePrompt()
+ }.goBack {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(trackingProtectionPage.url) {
+ verifyUrl(trackingProtectionPage.url.toString())
+ }.goToHomescreen {
+ }.openTopSiteTabWithTitle("Top Articles") {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561594
+ @SmokeTest
+ @Test
+ fun verifyUBlockWorksInPrivateModeTest() {
+ TestHelper.appContext.settings().shouldShowCookieBannersCFR = false
+ val addonName = "uBlock Origin"
+
+ addonsMenu {
+ installAddon(addonName, activityTestRule)
+ selectAllowInPrivateBrowsing()
+ closeAddonInstallCompletePrompt()
+ }.goBack {
+ }.openContextMenuOnSponsoredShortcut("Top Articles") {
+ }.openTopSiteInPrivateTab {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ openAddonsSubList()
+ verifyAddonAvailableInMainMenu(addonName)
+ verifyTrackersBlockedByUblock()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/875785
+ @Test
+ fun verifyUBlockWorksInNormalModeTest() {
+ val addonName = "uBlock Origin"
+
+ addonsMenu {
+ installAddon(addonName, activityTestRule)
+ closeAddonInstallCompletePrompt()
+ }.goBack {
+ }.openTopSiteTabWithTitle("Top Articles") {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ openAddonsSubList()
+ verifyTrackersBlockedByUblock()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt
new file mode 100644
index 0000000000..da889b435f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt
@@ -0,0 +1,313 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertYoutubeAppOpens
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the advanced section in Settings
+ *
+ */
+
+class SettingsAdvancedTest : TestSetup() {
+ private val youTubeSchemaLink = itemContainingText("Youtube schema link")
+ private val playStoreLink = itemContainingText("Playstore link")
+ private val playStoreUrl = "play.google.com"
+
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092699
+ // Walks through settings menu and sub-menus to ensure all items are present
+ @Test
+ fun verifyAdvancedSettingsSectionItemsTest() {
+ // ADVANCED
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsToolbar()
+ verifyAdvancedHeading()
+ verifyAddons()
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ verifyExternalDownloadManagerButton()
+ verifyExternalDownloadManagerToggle(false)
+ verifyLeakCanaryButton()
+ // LeakCanary is disabled in UI tests.
+ // See BuildConfig.LEAKCANARY.
+ verifyLeakCanaryToggle(false)
+ verifyRemoteDebuggingButton()
+ verifyRemoteDebuggingToggle(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2121046
+ // Assumes Youtube is installed and enabled
+ @SmokeTest
+ @Test
+ fun neverOpenLinkInAppTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ }.openOpenLinksInAppsMenu {
+ verifyOpenLinksInAppsView("Never")
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(playStoreLink)
+ waitForPageToLoad()
+ verifyUrl(playStoreUrl)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2121052
+ // Assumes Youtube is installed and enabled
+ @Test
+ fun privateBrowsingNeverOpenLinkInAppTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ }.openOpenLinksInAppsMenu {
+ verifyPrivateOpenLinksInAppsView("Never")
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(playStoreLink)
+ waitForPageToLoad()
+ verifyUrl(playStoreUrl)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2121045
+ // Assumes Youtube is installed and enabled
+ @SmokeTest
+ @Test
+ fun askBeforeOpeningLinkInAppCancelTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ }.openOpenLinksInAppsMenu {
+ verifyOpenLinksInAppsView("Never")
+ clickOpenLinkInAppOption("Ask before opening")
+ verifySelectedOpenLinksInAppOption("Ask before opening")
+ }.goBack {
+ verifySettingsOptionSummary("Open links in apps", "Ask before opening")
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(youTubeSchemaLink)
+ verifyOpenLinkInAnotherAppPrompt()
+ clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
+ verifyUrl(externalLinksPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2288347
+ // Assumes Youtube is installed and enabled
+ @SmokeTest
+ @Test
+ fun askBeforeOpeningLinkInAppOpenTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ }.openOpenLinksInAppsMenu {
+ verifyOpenLinksInAppsView("Never")
+ clickOpenLinkInAppOption("Ask before opening")
+ verifySelectedOpenLinksInAppOption("Ask before opening")
+ }.goBack {
+ verifySettingsOptionSummary("Open links in apps", "Ask before opening")
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(youTubeSchemaLink)
+ verifyOpenLinkInAnotherAppPrompt()
+ clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
+ mDevice.waitForIdle()
+ assertYoutubeAppOpens()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2121051
+ // Assumes Youtube is installed and enabled
+ @Test
+ fun privateBrowsingAskBeforeOpeningLinkInAppCancelTest() {
+ TestHelper.appContext.settings().shouldShowCookieBannersCFR = false
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ }.openOpenLinksInAppsMenu {
+ verifyPrivateOpenLinksInAppsView("Never")
+ clickOpenLinkInAppOption("Ask before opening")
+ verifySelectedOpenLinksInAppOption("Ask before opening")
+ }.goBack {
+ verifySettingsOptionSummary("Open links in apps", "Ask before opening")
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(youTubeSchemaLink)
+ verifyPrivateBrowsingOpenLinkInAnotherAppPrompt(
+ url = "youtube",
+ pageObject = youTubeSchemaLink,
+ )
+ clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
+ verifyUrl(externalLinksPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2288350
+ // Assumes Youtube is installed and enabled
+ @Test
+ fun privateBrowsingAskBeforeOpeningLinkInAppOpenTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ }.openOpenLinksInAppsMenu {
+ verifyPrivateOpenLinksInAppsView("Never")
+ clickOpenLinkInAppOption("Ask before opening")
+ verifySelectedOpenLinksInAppOption("Ask before opening")
+ }.goBack {
+ verifySettingsOptionSummary("Open links in apps", "Ask before opening")
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(youTubeSchemaLink)
+ verifyPrivateBrowsingOpenLinkInAnotherAppPrompt(
+ url = "youtube",
+ pageObject = youTubeSchemaLink,
+ )
+ clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
+ mDevice.waitForIdle()
+ assertYoutubeAppOpens()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1058618
+ // Assumes Youtube is installed and enabled
+ @Test
+ fun alwaysOpenLinkInAppTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyOpenLinksInAppsButton()
+ verifySettingsOptionSummary("Open links in apps", "Never")
+ }.openOpenLinksInAppsMenu {
+ verifyOpenLinksInAppsView("Never")
+ clickOpenLinkInAppOption("Always")
+ verifySelectedOpenLinksInAppOption("Always")
+ }.goBack {
+ verifySettingsOptionSummary("Open links in apps", "Always")
+ }
+
+ exitMenu()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(youTubeSchemaLink)
+ mDevice.waitForIdle()
+ assertYoutubeAppOpens()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1058617
+ @Test
+ fun dismissOpenLinksInAppCFRTest() {
+ activityIntentTestRule.applySettingsExceptions {
+ it.isOpenInAppBannerEnabled = true
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser("https://m.youtube.com/".toUri()) {
+ waitForPageToLoad()
+ verifyOpenLinksInAppsCFRExists(true)
+ clickOpenLinksInAppsDismissCFRButton()
+ verifyOpenLinksInAppsCFRExists(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2288331
+ @Test
+ fun goToSettingsFromOpenLinksInAppCFRTest() {
+ activityIntentTestRule.applySettingsExceptions {
+ it.isOpenInAppBannerEnabled = true
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser("https://m.youtube.com/".toUri()) {
+ waitForPageToLoad()
+ verifyOpenLinksInAppsCFRExists(true)
+ }.clickOpenLinksInAppsGoToSettingsCFRButton {
+ verifyOpenLinksInAppsButton()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsCustomizeTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsCustomizeTest.kt
new file mode 100644
index 0000000000..f20ff14182
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsCustomizeTest.kt
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.content.res.Configuration
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.verifyDarkThemeApplied
+import org.mozilla.fenix.helpers.TestHelper.verifyLightThemeApplied
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class SettingsCustomizeTest : TestSetup() {
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ private fun getUiTheme(): Boolean {
+ val mode =
+ activityIntentTestRule.activity.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)
+
+ return when (mode) {
+ Configuration.UI_MODE_NIGHT_YES -> true // dark theme is set
+ Configuration.UI_MODE_NIGHT_NO -> false // dark theme is not set, using light theme
+ else -> false // default option is light theme
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/344212
+ @Test
+ fun changeThemeOfTheAppTest() {
+ // Goes through the settings and changes the default search engine, then verifies it changes.
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ verifyThemes()
+ selectDarkMode()
+ verifyDarkThemeApplied(getUiTheme())
+ selectLightMode()
+ verifyLightThemeApplied(getUiTheme())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/466571
+ @Test
+ fun setToolbarPositionTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ verifyToolbarPositionPreference("Bottom")
+ clickTopToolbarToggle()
+ verifyToolbarPositionPreference("Top")
+ }.goBack {
+ }.goBack {
+ verifyToolbarPosition(defaultPosition = false)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ clickBottomToolbarToggle()
+ verifyToolbarPositionPreference("Bottom")
+ exitMenu()
+ }
+ homeScreen {
+ verifyToolbarPosition(defaultPosition = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1058682
+ @Test
+ fun turnOffSwipeToSwitchTabsPreferenceTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ verifySwipeToolbarGesturePrefState(true)
+ clickSwipeToolbarToSwitchTabToggle()
+ verifySwipeToolbarGesturePrefState(false)
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {
+ swipeNavBarRight(secondWebPage.url.toString())
+ verifyUrl(secondWebPage.url.toString())
+ swipeNavBarLeft(secondWebPage.url.toString())
+ verifyUrl(secondWebPage.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1992289
+ @Test
+ fun pullToRefreshPreferenceTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openCustomizeSubMenu {
+ verifyPullToRefreshGesturePrefState(isEnabled = true)
+ clickPullToRefreshToggle()
+ verifyPullToRefreshGesturePrefState(isEnabled = false)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt
new file mode 100644
index 0000000000..cae712884c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.Manifest
+import androidx.core.net.toUri
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.rule.GrantPermissionRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.downloadRobot
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the Settings for:
+ * Delete Browsing Data on quit
+ *
+ */
+class SettingsDeleteBrowsingDataOnQuitTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // Automatically allows app permissions, avoiding a system dialog showing up.
+ @get:Rule
+ val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+ Manifest.permission.RECORD_AUDIO,
+ )
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416048
+ @Test
+ fun deleteBrowsingDataOnQuitSettingTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ verifyNavigationToolBarHeader()
+ verifyDeleteBrowsingOnQuitEnabled(false)
+ verifyDeleteBrowsingOnQuitButtonSummary()
+ verifyDeleteBrowsingOnQuitEnabled(false)
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ verifyDeleteBrowsingOnQuitEnabled(true)
+ verifyAllTheCheckBoxesText()
+ verifyAllTheCheckBoxesChecked(true)
+ }.goBack {
+ verifySettingsOptionSummary("Delete browsing data on quit", "On")
+ }.goBack {
+ }.openThreeDotMenu {
+ verifyQuitButtonExists()
+ pressBack()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser("test".toUri()) {
+ }.openThreeDotMenu {
+ verifyQuitButtonExists()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416049
+ @Test
+ fun deleteOpenTabsOnQuitTest() {
+ val testPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.url) {
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ restartApp(activityTestRule)
+ }
+ navigationToolbar {
+ }.openTabTray {
+ verifyNoOpenTabsInNormalBrowsing()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416050
+ @Test
+ fun deleteBrowsingHistoryOnQuitTest() {
+ val genericPage =
+ getStorageTestAsset(mockWebServer, "generic1.html")
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ restartApp(activityTestRule)
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ exitMenu()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416051
+ @Test
+ fun deleteCookiesAndSiteDataOnQuitTest() {
+ val storageWritePage =
+ getStorageTestAsset(mockWebServer, "storage_write.html")
+ val storageCheckPage =
+ getStorageTestAsset(mockWebServer, "storage_check.html")
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage.url) {
+ clickPageObject(MatcherHelper.itemWithText("Set cookies"))
+ verifyPageContent("Values written to storage")
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ restartApp(activityTestRule)
+ }
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(storageCheckPage.url) {
+ verifyPageContent("Session storage empty")
+ verifyPageContent("Local storage empty")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage.url) {
+ verifyPageContent("No cookies set")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1243096
+ @SmokeTest
+ @Test
+ fun deleteDownloadsOnQuitTest() {
+ val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ downloadRobot {
+ openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip")
+ verifyDownloadCompleteNotificationPopup()
+ }.closeDownloadPrompt {
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ mDevice.waitForIdle()
+ }
+ restartApp(activityTestRule)
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openDownloadsManager {
+ verifyEmptyDownloadsList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416053
+ @SmokeTest
+ @Test
+ fun deleteSitePermissionsOnQuitTest() {
+ val testPage = "https://mozilla-mobile.github.io/testapp/permissions"
+ val testPageSubstring = "https://mozilla-mobile.github.io:443"
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton {
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Microphone not allowed")
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ mDevice.waitForIdle()
+ }
+ restartApp(activityTestRule)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton {
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416052
+ @Test
+ fun deleteCachedFilesOnQuitTest() {
+ val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingDataOnQuit {
+ clickDeleteBrowsingOnQuitButtonSwitch()
+ exitMenu()
+ }
+ homeScreen {
+ verifyExistingTopSitesTabs(pocketTopArticles)
+ }.openTopSiteTabWithTitle(pocketTopArticles) {
+ waitForPageToLoad()
+ }.goToHomescreen {
+ }.openThreeDotMenu {
+ clickQuit()
+ mDevice.waitForIdle()
+ }
+ // disabling wifi to prevent downloads in the background
+ setNetworkEnabled(enabled = false)
+ restartApp(activityTestRule)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser("about:cache".toUri()) {
+ verifyNetworkCacheIsEmpty("memory")
+ verifyNetworkCacheIsEmpty("disk")
+ }
+ setNetworkEnabled(enabled = true)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt
new file mode 100644
index 0000000000..a2dcf12695
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt
@@ -0,0 +1,254 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.settingsScreen
+
+/**
+ * Tests for verifying the Settings for:
+ * Delete Browsing Data
+ */
+
+class SettingsDeleteBrowsingDataTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/937561
+ @Test
+ fun deleteBrowsingDataOptionStatesTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyAllCheckBoxesAreChecked()
+ switchBrowsingHistoryCheckBox()
+ switchCachedFilesCheckBox()
+ verifyOpenTabsCheckBox(true)
+ verifyBrowsingHistoryDetails(false)
+ verifyCookiesCheckBox(true)
+ verifyCachedFilesCheckBox(false)
+ verifySitePermissionsCheckBox(true)
+ verifyDownloadsCheckBox(true)
+ }
+
+ restartApp(activityTestRule)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyOpenTabsCheckBox(true)
+ verifyBrowsingHistoryDetails(false)
+ verifyCookiesCheckBox(true)
+ verifyCachedFilesCheckBox(false)
+ verifySitePermissionsCheckBox(true)
+ verifyDownloadsCheckBox(true)
+ switchOpenTabsCheckBox()
+ switchBrowsingHistoryCheckBox()
+ switchCookiesCheckBox()
+ switchCachedFilesCheckBox()
+ switchSitePermissionsCheckBox()
+ switchDownloadsCheckBox()
+ verifyOpenTabsCheckBox(false)
+ verifyBrowsingHistoryDetails(true)
+ verifyCookiesCheckBox(false)
+ verifyCachedFilesCheckBox(true)
+ verifySitePermissionsCheckBox(false)
+ verifyDownloadsCheckBox(false)
+ }
+
+ restartApp(activityTestRule)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyOpenTabsCheckBox(false)
+ verifyBrowsingHistoryDetails(true)
+ verifyCookiesCheckBox(false)
+ verifyCachedFilesCheckBox(true)
+ verifySitePermissionsCheckBox(false)
+ verifyDownloadsCheckBox(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/517811
+ @Test
+ fun deleteOpenTabsBrowsingDataWithNoOpenTabsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyAllCheckBoxesAreChecked()
+ selectOnlyOpenTabsCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ }
+ settingsScreen {
+ verifyGeneralHeading()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/353531
+ @SmokeTest
+ @Test
+ fun deleteOpenTabsBrowsingDataTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.waitForIdle()
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyAllCheckBoxesAreChecked()
+ selectOnlyOpenTabsCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ clickDialogCancelButton()
+ verifyOpenTabsCheckBox(true)
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ }
+ settingsScreen {
+ verifyGeneralHeading()
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyOpenTabsDetails("0")
+ }.goBack {
+ }.goBack {
+ }.openTabDrawer {
+ verifyNoOpenTabsInNormalBrowsing()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/378864
+ @SmokeTest
+ @Test
+ fun deleteBrowsingHistoryTest() {
+ val genericPage = getStorageTestAsset(mockWebServer, "generic1.html").url
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage) {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ verifyBrowsingHistoryDetails("1")
+ selectOnlyBrowsingHistoryCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ clickDialogCancelButton()
+ verifyBrowsingHistoryDetails(true)
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ verifyBrowsingHistoryDetails("0")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ mDevice.pressBack()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416041
+ @SmokeTest
+ @Test
+ fun deleteCookiesAndSiteDataTest() {
+ val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val storageWritePage = getStorageTestAsset(mockWebServer, "storage_write.html").url
+ val storageCheckPage = getStorageTestAsset(mockWebServer, "storage_check.html").url
+
+ // Browsing a generic page to allow GV to load on a fresh run
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage) {
+ verifyPageContent("No cookies set")
+ clickPageObject(itemWithResId("setCookies"))
+ verifyPageContent("user=android")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageCheckPage) {
+ verifyPageContent("Session storage has value")
+ verifyPageContent("Local storage has value")
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ selectOnlyCookiesCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ clickDialogCancelButton()
+ verifyCookiesCheckBox(status = true)
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(storageCheckPage) {
+ verifyPageContent("Session storage empty")
+ verifyPageContent("Local storage empty")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(storageWritePage) {
+ verifyPageContent("No cookies set")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416042
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
+ @SmokeTest
+ @Test
+ fun deleteCachedFilesTest() {
+ val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles)
+
+ homeScreen {
+ verifyExistingTopSitesTabs(pocketTopArticles)
+ }.openTopSiteTabWithTitle(pocketTopArticles) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery("about:cache") {
+ // disabling wifi to prevent downloads in the background
+ setNetworkEnabled(enabled = false)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDeleteBrowsingData {
+ selectOnlyCachedFilesCheckBox()
+ clickDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataDialog()
+ confirmDeletionAndAssertSnackbar()
+ exitMenu()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ verifyNetworkCacheIsEmpty("memory")
+ verifyNetworkCacheIsEmpty("disk")
+ }
+ setNetworkEnabled(enabled = true)
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt
new file mode 100644
index 0000000000..5736fa14c1
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.FenixApplication
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper.getLoremIpsumAsset
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.checkTextSizeOnWebsite
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.util.FRENCH_LANGUAGE_HEADER
+import org.mozilla.fenix.ui.util.FRENCH_SYSTEM_LOCALE_OPTION
+import org.mozilla.fenix.ui.util.FR_SETTINGS
+import org.mozilla.fenix.ui.util.ROMANIAN_LANGUAGE_HEADER
+import java.util.Locale
+
+/**
+ * Tests for verifying the General section of the Settings menu
+ *
+ */
+class SettingsGeneralTest : TestSetup() {
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092697
+ @Test
+ fun verifyGeneralSettingsItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsToolbar()
+ verifyGeneralHeading()
+ verifySearchButton()
+ verifySettingsOptionSummary("Search", "Google")
+ verifyTabsButton()
+ verifySettingsOptionSummary("Tabs", "Close manually")
+ verifyHomepageButton()
+ verifySettingsOptionSummary("Homepage", "Open on homepage after four hours")
+ verifyCustomizeButton()
+ verifyLoginsAndPasswordsButton()
+ verifyAutofillButton()
+ verifyAccessibilityButton()
+ verifyLanguageButton()
+ verifySetAsDefaultBrowserButton()
+ verifyDefaultBrowserToggle(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/344213
+ @SmokeTest
+ @Test
+ fun verifyFontSizingChangeTest() {
+ // Goes through the settings and changes the default text on a webpage, then verifies if the text has changed.
+ val fenixApp = activityIntentTestRule.activity.applicationContext as FenixApplication
+ val webpage = getLoremIpsumAsset(mockWebServer).url
+
+ // This value will represent the text size percentage the webpage will scale to. The default value is 100%.
+ val textSizePercentage = 180
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAccessibilitySubMenu {
+ clickFontSizingSwitch()
+ verifyEnabledMenuItems()
+ changeTextSizeSlider(textSizePercentage)
+ verifyTextSizePercentage(textSizePercentage)
+ }.goBack {
+ }.goBack {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(webpage) {
+ checkTextSizeOnWebsite(textSizePercentage, fenixApp.components)
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAccessibilitySubMenu {
+ clickFontSizingSwitch()
+ verifyMenuItemsAreDisabled()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/516079
+ @SmokeTest
+ @Test
+ fun setAppLanguageDifferentThanSystemLanguageTest() {
+ val enLanguageHeaderText = getStringResource(R.string.preferences_language)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLanguageSubMenu {
+ registerAndCleanupIdlingResources(
+ RecyclerViewIdlingResource(
+ activityIntentTestRule.activity.findViewById(R.id.locale_list),
+ 2,
+ ),
+ ) {
+ selectLanguage("Romanian")
+ verifyLanguageHeaderIsTranslated(ROMANIAN_LANGUAGE_HEADER)
+ selectLanguage("Français")
+ verifyLanguageHeaderIsTranslated(FRENCH_LANGUAGE_HEADER)
+ selectLanguage(FRENCH_SYSTEM_LOCALE_OPTION)
+ verifyLanguageHeaderIsTranslated(enLanguageHeaderText)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/516080
+ @Test
+ fun searchInLanguagesListTest() {
+ val systemLocaleDefault = getStringResource(R.string.default_locale_text)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openLanguageSubMenu {
+ verifyLanguageListIsDisplayed()
+ openSearchBar()
+ typeInSearchBar("French")
+ verifySearchResultsContains(systemLocaleDefault)
+ clearSearchBar()
+ typeInSearchBar("French")
+ selectLanguageSearchResult("Français")
+ verifyLanguageHeaderIsTranslated(FRENCH_LANGUAGE_HEADER)
+ // Add this step when https://github.com/mozilla-mobile/fenix/issues/26733 is fixed
+ // verifyLanguageListIsDisplayed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/516078
+ // Because it requires changing system prefs, this test will run only on Debug builds
+ @Ignore("Failing due to app translation bug, see: https://github.com/mozilla-mobile/fenix/issues/26729")
+ @Test
+ fun verifyFollowDeviceLanguageTest() {
+ val frenchLocale = Locale("fr", "FR")
+
+ runWithSystemLocaleChanged(frenchLocale, activityIntentTestRule) {
+ mDevice.waitForIdle(waitingTimeLong)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings(localizedText = FR_SETTINGS) {
+ }.openLanguageSubMenu(localizedText = FRENCH_LANGUAGE_HEADER) {
+ verifyLanguageHeaderIsTranslated(FRENCH_LANGUAGE_HEADER)
+ verifySelectedLanguage(FRENCH_SYSTEM_LOCALE_OPTION)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1360557
+ @Test
+ fun tabsSettingsMenuItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyTabsButton()
+ verifySettingsOptionSummary("Tabs", "Close manually")
+ }.openTabsSubMenu {
+ verifyTabViewOptions()
+ verifyCloseTabsOptions()
+ verifyMoveOldTabsToInactiveOptions()
+ verifySelectedCloseTabsOption("Never")
+ clickClosedTabsOption("After one day")
+ verifySelectedCloseTabsOption("After one day")
+ }.goBack {
+ verifySettingsOptionSummary("Tabs", "Close after one day")
+ }.openTabsSubMenu {
+ clickClosedTabsOption("After one week")
+ verifySelectedCloseTabsOption("After one week")
+ }.goBack {
+ verifySettingsOptionSummary("Tabs", "Close after one week")
+ }.openTabsSubMenu {
+ clickClosedTabsOption("After one month")
+ verifySelectedCloseTabsOption("After one month")
+ }.goBack {
+ verifySettingsOptionSummary("Tabs", "Close after one month")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243583
+ // For API>23
+ // Verifies the default browser switch opens the system default apps menu.
+ @SmokeTest
+ @Test
+ fun changeDefaultBrowserSetting() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifyDefaultBrowserToggle(false)
+ clickDefaultBrowserSwitch()
+ verifyAndroidDefaultAppsMenuAppears()
+ }
+ // Dismiss the request
+ mDevice.pressBack()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHTTPSOnlyModeTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHTTPSOnlyModeTest.kt
new file mode 100644
index 0000000000..9636810e3b
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHTTPSOnlyModeTest.kt
@@ -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.ui
+
+import androidx.core.net.toUri
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class SettingsHTTPSOnlyModeTest : TestSetup() {
+ private val httpPageUrl = "http://example.com/"
+ private val httpsPageUrl = "https://example.com/"
+ private val insecureHttpPage = "http.badssl.com"
+
+ // "HTTPs not supported" error page contents:
+ private val httpsOnlyErrorTitle = "Secure site not available"
+ private val httpsOnlyErrorMessage = "Most likely, the website simply does not support HTTPS."
+ private val httpsOnlyErrorMessage2 = "However, it’s also possible that an attacker is involved. If you continue to the website, you should not enter any sensitive info. If you continue, HTTPS-Only mode will be turned off temporarily for the site."
+ private val httpsOnlyContinueButton = "Continue to HTTP Site"
+ private val httpsOnlyBackButton = "Go Back (Recommended)"
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1724825
+ @Test
+ fun httpsOnlyModeMenuItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHttpsOnlyModeMenu {
+ verifyHttpsOnlyModeMenuHeader()
+ verifyHttpsOnlyModeSummary()
+ verifyHttpsOnlyModeIsEnabled(false)
+ verifyHttpsOnlyModeOptionsEnabled(false)
+ verifyHttpsOnlyOptionSelected(
+ allTabsOptionSelected = false,
+ privateTabsOptionSelected = false,
+ )
+ clickHttpsOnlyModeSwitch()
+ verifyHttpsOnlyModeIsEnabled(true)
+ verifyHttpsOnlyModeOptionsEnabled(true)
+ verifyHttpsOnlyOptionSelected(
+ allTabsOptionSelected = true,
+ privateTabsOptionSelected = false,
+ )
+ }.goBack {
+ verifySettingsToolbar()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1724827
+ @SmokeTest
+ @Test
+ fun httpsOnlyModeEnabledInNormalBrowsingTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHttpsOnlyModeMenu {
+ clickHttpsOnlyModeSwitch()
+ verifyHttpsOnlyOptionSelected(
+ allTabsOptionSelected = true,
+ privateTabsOptionSelected = false,
+ )
+ }.goBack {
+ verifySettingsOptionSummary("HTTPS-Only Mode", "On in all tabs")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ verifyUrl(httpsPageUrl)
+ }.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
+ verifyPageContent(httpsOnlyErrorTitle)
+ verifyPageContent(httpsOnlyErrorMessage)
+ verifyPageContent(httpsOnlyErrorMessage2)
+ verifyPageContent(httpsOnlyBackButton)
+ clickPageObject(itemContainingText(httpsOnlyBackButton))
+ verifyPageContent("Example Domain")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
+ clickPageObject(itemContainingText(httpsOnlyContinueButton))
+ verifyPageContent("http.badssl.com")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2091057
+ @Test
+ fun httpsOnlyModeExceptionPersistsForCurrentSessionTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHttpsOnlyModeMenu {
+ clickHttpsOnlyModeSwitch()
+ verifyHttpsOnlyOptionSelected(
+ allTabsOptionSelected = true,
+ privateTabsOptionSelected = false,
+ )
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
+ verifyPageContent(httpsOnlyErrorTitle)
+ clickPageObject(itemContainingText(httpsOnlyContinueButton))
+ verifyPageContent("http.badssl.com")
+ }.openTabDrawer {
+ closeTab()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
+ verifyPageContent("http.badssl.com")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1724828
+ @Test
+ fun httpsOnlyModeEnabledOnlyInPrivateBrowsingTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHttpsOnlyModeMenu {
+ clickHttpsOnlyModeSwitch()
+ selectHttpsOnlyModeOption(
+ allTabsOptionSelected = false,
+ privateTabsOptionSelected = true,
+ )
+ }.goBack {
+ verifySettingsOptionSummary("HTTPS-Only Mode", "On in private tabs")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
+ verifyPageContent("http.badssl.com")
+ }.goToHomescreen {
+ }.togglePrivateBrowsingMode()
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ verifyUrl(httpsPageUrl)
+ }.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
+ verifyPageContent(httpsOnlyErrorTitle)
+ verifyPageContent(httpsOnlyErrorMessage)
+ verifyPageContent(httpsOnlyErrorMessage2)
+ verifyPageContent(httpsOnlyBackButton)
+ clickPageObject(itemContainingText(httpsOnlyBackButton))
+ verifyPageContent("Example Domain")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(insecureHttpPage.toUri()) {
+ clickPageObject(itemContainingText(httpsOnlyContinueButton))
+ verifyPageContent("http.badssl.com")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2091058
+ @Test
+ fun turnOffHttpsOnlyModeTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHttpsOnlyModeMenu {
+ clickHttpsOnlyModeSwitch()
+ verifyHttpsOnlyOptionSelected(
+ allTabsOptionSelected = true,
+ privateTabsOptionSelected = false,
+ )
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ verifyUrl(httpsPageUrl)
+ }.goBackToBrowserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHttpsOnlyModeMenu {
+ clickHttpsOnlyModeSwitch()
+ verifyHttpsOnlyModeIsEnabled(false)
+ }.goBack {
+ verifySettingsOptionSummary("HTTPS-Only Mode", "Off")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(httpPageUrl.toUri()) {
+ waitForPageToLoad()
+ }.openNavigationToolbar {
+ verifyUrl(httpPageUrl)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt
new file mode 100644
index 0000000000..8dc31c64e8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.openAppFromExternalLink
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the Homepage settings menu
+ *
+ */
+class SettingsHomepageTest : TestSetup() {
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1564843
+ @Test
+ fun verifyHomepageSettingsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHomepageSubMenu {
+ verifyHomePageView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1564859
+ @Test
+ fun verifyShortcutOptionTest() {
+ // en-US defaults
+ val defaultTopSites = arrayOf(
+ "Top Articles",
+ "Wikipedia",
+ "Google",
+ )
+ val genericURL = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ defaultTopSites.forEach { item ->
+ verifyExistingTopSitesTabs(item)
+ }
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ clickShortcutsButton()
+ }.goBackToHomeScreen {
+ defaultTopSites.forEach { item ->
+ verifyNotExistingTopSitesList(item)
+ }
+ }
+ // Disabling the "Shortcuts" homepage setting option should remove the "Add to shortcuts" from main menu option
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(shouldExist = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1565003
+ @Test
+ fun verifyRecentlyVisitedOptionTest() {
+ activityIntentTestRule.applySettingsExceptions {
+ it.isRecentTabsFeatureEnabled = false
+ }
+ val genericURL = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.goToHomescreen {
+ verifyRecentlyVisitedSectionIsDisplayed(true)
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ clickRecentlyVisited()
+ }.goBackToHomeScreen {
+ verifyRecentlyVisitedSectionIsDisplayed(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1564999
+ @SmokeTest
+ @Test
+ fun jumpBackInOptionTest() {
+ val genericURL = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.goToHomescreen {
+ verifyJumpBackInSectionIsDisplayed()
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ clickJumpBackInButton()
+ }.goBackToHomeScreen {
+ verifyJumpBackInSectionIsNotDisplayed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1565000
+ @SmokeTest
+ @Test
+ fun recentBookmarksOptionTest() {
+ val genericURL = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openThreeDotMenu {
+ }.bookmarkPage {
+ }.goToHomescreen {
+ verifyRecentBookmarksSectionIsDisplayed(exists = true)
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ clickRecentBookmarksButton()
+ }.goBackToHomeScreen {
+ verifyRecentBookmarksSectionIsDisplayed(exists = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569831
+ @SmokeTest
+ @Test
+ fun verifyOpeningScreenOptionsTest() {
+ val genericURL = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsOptionSummary("Homepage", "Open on homepage after four hours")
+ }.openHomepageSubMenu {
+ verifySelectedOpeningScreenOption("Homepage after four hours of inactivity")
+ clickOpeningScreenOption("Homepage")
+ verifySelectedOpeningScreenOption("Homepage")
+ }
+
+ restartApp(activityIntentTestRule)
+
+ homeScreen {
+ verifyHomeScreen()
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsOptionSummary("Homepage", "Open on homepage")
+ }.openHomepageSubMenu {
+ clickOpeningScreenOption("Last tab")
+ verifySelectedOpeningScreenOption("Last tab")
+ }.goBack {
+ verifySettingsOptionSummary("Homepage", "Open on last tab")
+ }
+
+ restartApp(activityIntentTestRule)
+
+ browserScreen {
+ verifyUrl(genericURL.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569843
+ @Test
+ fun verifyOpeningScreenAfterLaunchingExternalLinkTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openHomepageSubMenu {
+ clickOpeningScreenOption("Homepage")
+ }.goBackToHomeScreen {}
+
+ with(activityIntentTestRule) {
+ finishActivity()
+ mDevice.waitForIdle()
+ this.applySettingsExceptions {
+ it.isTCPCFREnabled = false
+ }
+ openAppFromExternalLink(genericPage.url.toString())
+ }
+
+ browserScreen {
+ verifyPageContent(genericPage.content)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1676359
+ @Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/26559")
+ @Test
+ fun verifyWallpaperChangeTest() {
+ val wallpapers = listOf(
+ "Wallpaper Item: amethyst",
+ "Wallpaper Item: cerulean",
+ "Wallpaper Item: sunrise",
+ )
+
+ for (wallpaper in wallpapers) {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ openWallpapersMenu()
+ selectWallpaper(wallpaper)
+ verifySnackBarText("Wallpaper updated!")
+ }.clickSnackBarViewButton {
+ verifyWallpaperImageApplied(true)
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt
new file mode 100644
index 0000000000..93084fb3ea
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.notificationShade
+
+/**
+ * Tests for verifying the the privacy and security section of the Settings menu
+ *
+ */
+
+class SettingsPrivacyTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092698
+ @Test
+ fun settingsPrivacyItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsToolbar()
+ verifyPrivacyHeading()
+ verifyPrivateBrowsingButton()
+ verifyHTTPSOnlyModeButton()
+ verifySettingsOptionSummary("HTTPS-Only Mode", "Off")
+ verifySettingsOptionSummary("Cookie Banner Blocker in private browsing", "")
+ verifyEnhancedTrackingProtectionButton()
+ verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard")
+ verifySitePermissionsButton()
+ verifyDeleteBrowsingDataButton()
+ verifyDeleteBrowsingDataOnQuitButton()
+ verifySettingsOptionSummary("Delete browsing data on quit", "Off")
+ verifyNotificationsButton()
+ verifySettingsOptionSummary("Notifications", "Allowed")
+ verifyDataCollectionButton()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243362
+ @Test
+ fun verifyDataCollectionSettingsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuDataCollection {
+ verifyDataCollectionView(
+ true,
+ true,
+ "On",
+ )
+ clickUsageAndTechnicalDataToggle()
+ verifyUsageAndTechnicalDataToggle(false)
+ clickUsageAndTechnicalDataToggle()
+ verifyUsageAndTechnicalDataToggle(true)
+ clickMarketingDataToggle()
+ verifyMarketingDataToggle(false)
+ clickMarketingDataToggle()
+ verifyMarketingDataToggle(true)
+ clickStudiesOption()
+ verifyStudiesToggle(true)
+ clickStudiesToggle()
+ verifyStudiesDialog()
+ clickStudiesDialogCancelButton()
+ verifyStudiesToggle(true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1024594
+ @Test
+ fun verifyNotificationsSettingsTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ // Clear all existing notifications
+ notificationShade {
+ mDevice.openNotification()
+ clearNotifications()
+ }
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openNotificationShade {
+ verifySystemNotificationExists("Close private tabs")
+ }.closeNotificationTray {
+ }.openThreeDotMenu {
+ }.openSettings {
+ verifySettingsOptionSummary("Notifications", "Allowed")
+ }.openSettingsSubMenuNotifications {
+ verifyAllSystemNotificationsToggleState(true)
+ verifyPrivateBrowsingSystemNotificationsToggleState(true)
+ clickPrivateBrowsingSystemNotificationsToggle()
+ verifyPrivateBrowsingSystemNotificationsToggleState(false)
+ clickAllSystemNotificationsToggle()
+ verifyAllSystemNotificationsToggleState(false)
+ }.goBack {
+ verifySettingsOptionSummary("Notifications", "Not allowed")
+ }.goBackToBrowser {
+ }.openNotificationShade {
+ verifySystemNotificationDoesNotExist("Close private tabs")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivateBrowsingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivateBrowsingTest.kt
new file mode 100644
index 0000000000..033addb4c6
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivateBrowsingTest.kt
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.AppAndSystemHelper.openAppFromExternalLink
+import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.addToHomeScreen
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+class SettingsPrivateBrowsingTest : TestSetup() {
+ private val pageShortcutName = generateRandomString(5)
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/555822
+ @Test
+ fun verifyPrivateBrowsingMenuItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ verifyAddPrivateBrowsingShortcutButton()
+ verifyOpenLinksInPrivateTab()
+ verifyOpenLinksInPrivateTabOff()
+ }.goBack {
+ verifySettingsView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/420086
+ @Test
+ fun launchLinksInAPrivateTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ setOpenLinksInPrivateOn()
+
+ openAppFromExternalLink(firstWebPage.url.toString())
+
+ browserScreen {
+ verifyUrl(firstWebPage.url.toString())
+ }.openTabDrawer {
+ verifyPrivateModeSelected()
+ }.closeTabDrawer {
+ }.goToHomescreen { }
+
+ setOpenLinksInPrivateOff()
+
+ // We need to open a different link, otherwise it will open the same session
+ openAppFromExternalLink(secondWebPage.url.toString())
+
+ browserScreen {
+ verifyUrl(secondWebPage.url.toString())
+ }.openTabDrawer {
+ verifyNormalModeSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/555776
+ @Test
+ fun launchPageShortcutInPrivateBrowsingTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ setOpenLinksInPrivateOn()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openThreeDotMenu {
+ }.openAddToHomeScreen {
+ addShortcutName(pageShortcutName)
+ clickAddShortcutButton()
+ clickAddAutomaticallyButton()
+ verifyShortcutAdded(pageShortcutName)
+ }
+
+ mDevice.waitForIdle()
+ // We need to close the existing tab here, to open a different session
+ restartApp(activityTestRule)
+
+ browserScreen {
+ }.openTabDrawer {
+ verifyNormalModeSelected()
+ closeTab()
+ }
+
+ addToHomeScreen {
+ }.searchAndOpenHomeScreenShortcut(pageShortcutName) {
+ }.openTabDrawer {
+ verifyPrivateModeSelected()
+ closeTab()
+ }
+
+ setOpenLinksInPrivateOff()
+
+ addToHomeScreen {
+ }.searchAndOpenHomeScreenShortcut(pageShortcutName) {
+ }.openTabDrawer {
+ verifyNormalModeSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414583
+ @Test
+ fun addPrivateBrowsingShortcutFromSettingsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ cancelPrivateShortcutAddition()
+ addPrivateShortcutToHomescreen()
+ verifyPrivateBrowsingShortcutIcon()
+ }.openPrivateBrowsingShortcut {
+ verifySearchView()
+ }.openBrowser {
+ }.openTabDrawer {
+ verifyPrivateModeSelected()
+ }
+ }
+}
+
+private fun setOpenLinksInPrivateOn() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ verifyOpenLinksInPrivateTabEnabled()
+ clickOpenLinksInPrivateTabSwitch()
+ }.goBack {
+ }.goBack {
+ verifyHomeComponent()
+ }
+}
+
+private fun setOpenLinksInPrivateOff() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openPrivateBrowsingSubMenu {
+ clickOpenLinksInPrivateTabSwitch()
+ verifyOpenLinksInPrivateTabOff()
+ }.goBack {
+ }.goBack {
+ verifyHomeComponent()
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt
new file mode 100644
index 0000000000..5abbaa8b4a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt
@@ -0,0 +1,634 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.espresso.Espresso.pressBack
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
+import org.mozilla.fenix.helpers.AppAndSystemHelper.setSystemLocale
+import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.addCustomSearchEngine
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createBookmarkItem
+import org.mozilla.fenix.helpers.MockBrowserDataHelper.createHistoryItem
+import org.mozilla.fenix.helpers.SearchDispatcher
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.EngineShortcut
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.searchScreen
+import java.util.Locale
+
+class SettingsSearchTest : TestSetup() {
+ private lateinit var searchMockServer: MockWebServer
+ private val defaultSearchEngineList =
+ listOf(
+ "Bing",
+ "DuckDuckGo",
+ "Google",
+ )
+
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityIntentTestRule.withDefaultSettingsOverrides(),
+ ) { it.activity }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ searchMockServer = MockWebServer().apply {
+ dispatcher = SearchDispatcher()
+ start()
+ }
+ }
+
+ @After
+ override fun tearDown() {
+ super.tearDown()
+ searchMockServer.shutdown()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203333
+ @Test
+ fun verifySearchSettingsMenuItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ verifyToolbarText("Search")
+ verifySearchEnginesSectionHeader()
+ verifyDefaultSearchEngineHeader()
+ verifyDefaultSearchEngineSummary("Google")
+ verifyManageSearchShortcutsHeader()
+ verifyManageShortcutsSummary()
+ verifyAddressBarSectionHeader()
+ verifyAutocompleteURlsIsEnabled(true)
+ verifyShowClipboardSuggestionsEnabled(true)
+ verifySearchBrowsingHistoryEnabled(true)
+ verifySearchBookmarksEnabled(true)
+ verifySearchSyncedTabsEnabled(true)
+ verifyVoiceSearchEnabled(true)
+ verifyShowSearchSuggestionsEnabled(true)
+ verifyShowSearchSuggestionsInPrivateEnabled(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203307
+ @Test
+ fun verifyDefaultSearchEnginesSettingsItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ verifyDefaultSearchEngineHeader()
+ openDefaultSearchEngineMenu()
+ verifyToolbarText("Default search engine")
+ verifyDefaultSearchEngineList()
+ verifyDefaultSearchEngineSelected("Google")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203308
+ @SmokeTest
+ @Test
+ fun verifyTheDefaultSearchEngineCanBeChangedTest() {
+ // Goes through the settings and changes the default search engine, then verifies it has changed.
+ defaultSearchEngineList.forEach {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ changeDefaultSearchEngine(it)
+ exitMenu()
+ }
+ searchScreen {
+ verifySearchEngineIcon(it)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233586
+ @Test
+ fun verifyUrlAutocompleteToggleTest() {
+ homeScreen {
+ }.openSearch {
+ typeSearch("mo")
+ verifyTypedToolbarText("monster.com")
+ typeSearch("moz")
+ verifyTypedToolbarText("mozilla.org")
+ }.dismissSearchBar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ toggleAutocomplete()
+ }.goBack {
+ }.goBack {
+ }.openSearch {
+ typeSearch("moz")
+ verifyTypedToolbarText("moz")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/361817
+ @Test
+ fun disableSearchBrowsingHistorySuggestionsToggleTest() {
+ val websiteURL = getGenericAsset(mockWebServer, 1).url.toString()
+
+ createHistoryItem(websiteURL)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ switchSearchHistoryToggle()
+ exitMenu()
+ }
+
+ homeScreen {
+ }.openSearch {
+ typeSearch("test")
+ verifySuggestionsAreNotDisplayed(
+ activityTestRule,
+ "Firefox Suggest",
+ websiteURL,
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/412926
+ @Test
+ fun disableSearchBookmarksToggleTest() {
+ val website = getGenericAsset(mockWebServer, 1)
+
+ createBookmarkItem(website.url.toString(), website.title, 1u)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ switchSearchBookmarksToggle()
+ // We want to avoid confusion between history and bookmarks searches,
+ // so we'll disable this too.
+ switchSearchHistoryToggle()
+ exitMenu()
+ }
+
+ homeScreen {
+ }.openSearch {
+ typeSearch("test")
+ verifySuggestionsAreNotDisplayed(
+ activityTestRule,
+ "Firefox Suggest",
+ website.title,
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203309
+ // Verifies setting as default a customized search engine name and URL
+ @SmokeTest
+ @Test
+ fun verifyCustomSearchEngineCanBeAddedFromSearchEngineMenuTest() {
+ val customSearchEngine = object {
+ val title = "TestSearchEngine"
+ val url = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ openAddSearchEngineMenu()
+ verifySaveSearchEngineButtonEnabled(false)
+ typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.url)
+ verifySaveSearchEngineButtonEnabled(true)
+ saveNewSearchEngine()
+ verifySnackBarText("Created ${customSearchEngine.title}")
+ verifyEngineListContains(customSearchEngine.title, shouldExist = true)
+ openEngineOverflowMenu(customSearchEngine.title)
+ pressBack()
+ changeDefaultSearchEngine(customSearchEngine.title)
+ pressBack()
+ openManageShortcutsMenu()
+ verifyEngineListContains(customSearchEngine.title, shouldExist = true)
+ pressBack()
+ }.goBack {
+ verifySettingsOptionSummary("Search", customSearchEngine.title)
+ }.goBack {
+ }.openSearch {
+ verifySearchEngineIcon(customSearchEngine.title)
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(customSearchEngine.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203335
+ @Test
+ fun addCustomSearchEngineToManageShortcutsListTest() {
+ val customSearchEngine = object {
+ val title = "TestSearchEngine"
+ val url = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openManageShortcutsMenu()
+ openAddSearchEngineMenu()
+ typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.url)
+ saveNewSearchEngine()
+ verifyEngineListContains(customSearchEngine.title, shouldExist = true)
+ pressBack()
+ openDefaultSearchEngineMenu()
+ verifyEngineListContains(customSearchEngine.title, shouldExist = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203343
+ @Test
+ fun verifyLearnMoreLinksFromAddSearchEngineSectionTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ openAddSearchEngineMenu()
+ }.clickCustomSearchStringLearnMoreLink {
+ verifyUrl(
+ "support.mozilla.org/en-US/kb/manage-my-default-search-engines-firefox-android?as=u&utm_source=inproduct",
+ )
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ openAddSearchEngineMenu()
+ }.clickCustomSearchSuggestionsLearnMoreLink {
+ verifyUrl(
+ "support.mozilla.org/en-US/kb/manage-my-default-search-engines-firefox-android?as=u&utm_source=inproduct",
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203310
+ @Test
+ fun editCustomSearchEngineTest() {
+ val customSearchEngine = object {
+ val title = "TestSearchEngine"
+ val url = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
+ val newTitle = "NewEngineTitle"
+ }
+
+ addCustomSearchEngine(searchMockServer, customSearchEngine.title)
+ restartApp(activityTestRule.activityRule)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ verifyEngineListContains(customSearchEngine.title, shouldExist = true)
+ openEngineOverflowMenu(customSearchEngine.title)
+ clickEdit()
+ typeCustomEngineDetails(customSearchEngine.newTitle, customSearchEngine.url)
+ saveEditSearchEngine()
+ verifySnackBarText("Saved ${customSearchEngine.newTitle}")
+ verifyEngineListContains(customSearchEngine.newTitle, shouldExist = true)
+ pressBack()
+ openManageShortcutsMenu()
+ verifyEngineListContains(customSearchEngine.newTitle, shouldExist = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203312
+ @Test
+ fun verifyErrorMessagesForInvalidSearchEngineUrlsTest() {
+ val customSearchEngine = object {
+ val title = "TestSearchEngine"
+ val badTemplateUrl = "http://localhost:${searchMockServer.port}/searchResults.html?search="
+ val typoUrl = "http://local:${searchMockServer.port}/searchResults.html?search=%s"
+ val goodUrl = "http://localhost:${searchMockServer.port}/searchResults.html?search=%s"
+ }
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ openAddSearchEngineMenu()
+ typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.badTemplateUrl)
+ saveNewSearchEngine()
+ verifyInvalidTemplateSearchStringFormatError()
+ typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.typoUrl)
+ saveNewSearchEngine()
+ verifyErrorConnectingToSearchString(customSearchEngine.title)
+ typeCustomEngineDetails(customSearchEngine.title, customSearchEngine.goodUrl)
+ typeSearchEngineSuggestionString(customSearchEngine.badTemplateUrl)
+ saveNewSearchEngine()
+ verifyInvalidTemplateSearchStringFormatError()
+ typeSearchEngineSuggestionString(customSearchEngine.typoUrl)
+ saveNewSearchEngine()
+ verifyErrorConnectingToSearchString(customSearchEngine.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203313
+ @Test
+ fun deleteCustomSearchEngineTest() {
+ val customSearchEngineTitle = "TestSearchEngine"
+
+ addCustomSearchEngine(mockWebServer, searchEngineName = customSearchEngineTitle)
+ restartApp(activityTestRule.activityRule)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openDefaultSearchEngineMenu()
+ verifyEngineListContains(customSearchEngineTitle, shouldExist = true)
+ openEngineOverflowMenu(customSearchEngineTitle)
+ clickDeleteSearchEngine()
+ verifySnackBarText("Deleted $customSearchEngineTitle")
+ clickSnackbarButton("UNDO")
+ verifyEngineListContains(customSearchEngineTitle, shouldExist = true)
+ changeDefaultSearchEngine(customSearchEngineTitle)
+ openEngineOverflowMenu(customSearchEngineTitle)
+ clickDeleteSearchEngine()
+ verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
+ verifyDefaultSearchEngineSelected("Google")
+ pressBack()
+ openManageShortcutsMenu()
+ verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
+ exitMenu()
+ }
+ searchScreen {
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(customSearchEngineTitle, shouldExist = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203339
+ @Test
+ fun deleteCustomSearchShortcutTest() {
+ val customSearchEngineTitle = "TestSearchEngine"
+
+ addCustomSearchEngine(mockWebServer, searchEngineName = customSearchEngineTitle)
+ restartApp(activityTestRule.activityRule)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openManageShortcutsMenu()
+ verifyEngineListContains(customSearchEngineTitle, shouldExist = true)
+ openCustomShortcutOverflowMenu(activityTestRule, customSearchEngineTitle)
+ clickDeleteSearchEngine(activityTestRule)
+ verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
+ pressBack()
+ openDefaultSearchEngineMenu()
+ verifyEngineListContains(customSearchEngineTitle, shouldExist = false)
+ exitMenu()
+ }
+ searchScreen {
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(customSearchEngineTitle, shouldExist = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233588
+ // Test running on beta/release builds in CI:
+ // caution when making changes to it, so they don't block the builds
+ // Goes through the settings and changes the search suggestion toggle, then verifies it changes.
+ @SmokeTest
+ @Test
+ fun verifyShowSearchSuggestionsToggleTest() {
+ homeScreen {
+ }.openSearch {
+ // The Google related suggestions aren't always displayed on cold run
+ // Bugzilla ticket: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod("DuckDuckGo")
+ typeSearch("mozilla ")
+ verifySearchEngineSuggestionResults(
+ activityTestRule,
+ "mozilla firefox",
+ searchTerm = "mozilla ",
+ )
+ }.dismissSearchBar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ toggleShowSearchSuggestions()
+ }.goBack {
+ }.goBack {
+ }.openSearch {
+ // The Google related suggestions aren't always displayed on cold run
+ // Bugzilla ticket: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
+ clickSearchSelectorButton()
+ selectTemporarySearchMethod("DuckDuckGo")
+ typeSearch("mozilla")
+ verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/464420
+ // Tests the "Don't allow" option from private mode search suggestions onboarding dialog
+ @Test
+ fun doNotAllowSearchSuggestionsInPrivateBrowsingTest() {
+ homeScreen {
+ togglePrivateBrowsingModeOnOff()
+ }.openSearch {
+ typeSearch("mozilla")
+ verifyAllowSuggestionsInPrivateModeDialog()
+ denySuggestionsInPrivateMode()
+ verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1957063
+ // Tests the "Allow" option from private mode search suggestions onboarding dialog
+ @Test
+ fun allowSearchSuggestionsInPrivateBrowsingTest() {
+ homeScreen {
+ togglePrivateBrowsingModeOnOff()
+ }.openSearch {
+ typeSearch("mozilla")
+ verifyAllowSuggestionsInPrivateModeDialog()
+ allowSuggestionsInPrivateMode()
+ verifySearchEngineSuggestionResults(
+ activityTestRule,
+ "mozilla firefox",
+ searchTerm = "mozilla",
+ )
+ }.dismissSearchBar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ switchShowSuggestionsInPrivateSessionsToggle()
+ }.goBack {
+ }.goBack {
+ }.openSearch {
+ typeSearch("mozilla")
+ verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/888673
+ @Test
+ fun verifyShowVoiceSearchToggleTest() {
+ homeScreen {
+ }.openSearch {
+ verifyVoiceSearchButtonVisibility(true)
+ startVoiceSearch()
+ }.dismissSearchBar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ toggleVoiceSearch()
+ exitMenu()
+ }
+ homeScreen {
+ }.openSearch {
+ verifyVoiceSearchButtonVisibility(false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/412927
+ @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
+ @Test
+ fun verifyShowClipboardSuggestionsToggleTest() {
+ val link = "https://www.mozilla.org/en-US/"
+ setTextToClipBoard(appContext, link)
+
+ homeScreen {
+ }.openNavigationToolbar {
+ verifyClipboardSuggestionsAreDisplayed(link, true)
+ }.visitLinkFromClipboard {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ }.openNewTab {
+ }
+ navigationToolbar {
+ // After visiting the link from clipboard it shouldn't be displayed again
+ verifyClipboardSuggestionsAreDisplayed(shouldBeDisplayed = false)
+ }.goBackToHomeScreen {
+ setTextToClipBoard(appContext, link)
+ }.openTabDrawer {
+ }.openNewTab {
+ }
+ navigationToolbar {
+ verifyClipboardSuggestionsAreDisplayed(link, true)
+ }.goBackToHomeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ verifyShowClipboardSuggestionsEnabled(true)
+ toggleClipboardSuggestion()
+ verifyShowClipboardSuggestionsEnabled(false)
+ exitMenu()
+ }
+ homeScreen {
+ }.openTabDrawer {
+ }.openNewTab {
+ }
+ navigationToolbar {
+ verifyClipboardSuggestionsAreDisplayed(link, false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2233337
+ @Test
+ fun verifyTheSearchEnginesListsRespectTheLocaleTest() {
+ runWithSystemLocaleChanged(Locale.CHINA, activityTestRule.activityRule) {
+ // Checking search engines for CH locale
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(
+ "Google",
+ "百度",
+ "Bing",
+ "DuckDuckGo",
+ )
+ }.dismissSearchBar {}
+
+ // Checking search engines for FR locale
+ setSystemLocale(Locale.FRENCH)
+ homeScreen {
+ }.openSearch {
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains(
+ "Google",
+ "Bing",
+ "DuckDuckGo",
+ "Qwant",
+ "Wikipédia (fr)",
+ )
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203334
+ @Test
+ fun verifyManageSearchShortcutsSettingsItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openManageShortcutsMenu()
+ verifyToolbarText("Manage alternative search engines")
+ verifyEnginesShortcutsListHeader()
+ verifyManageShortcutsList(activityTestRule)
+ verifySearchShortcutChecked(
+ EngineShortcut(name = "Google", checkboxIndex = 1, isChecked = true),
+ EngineShortcut(name = "Bing", checkboxIndex = 4, isChecked = true),
+ EngineShortcut(name = "Amazon.com", checkboxIndex = 7, isChecked = true),
+ EngineShortcut(name = "DuckDuckGo", checkboxIndex = 10, isChecked = true),
+ EngineShortcut(name = "eBay", checkboxIndex = 13, isChecked = true),
+ EngineShortcut(name = "Wikipedia", checkboxIndex = 16, isChecked = true),
+ EngineShortcut(name = "Reddit", checkboxIndex = 19, isChecked = false),
+ EngineShortcut(name = "YouTube", checkboxIndex = 22, isChecked = false),
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203340
+ @SmokeTest
+ @Test
+ fun verifySearchShortcutChangesAreReflectedInSearchSelectorMenuTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSearchSubMenu {
+ openManageShortcutsMenu()
+ selectSearchShortcut(EngineShortcut(name = "Google", checkboxIndex = 1))
+ selectSearchShortcut(EngineShortcut(name = "Amazon.com", checkboxIndex = 7))
+ selectSearchShortcut(EngineShortcut(name = "Reddit", checkboxIndex = 19))
+ selectSearchShortcut(EngineShortcut(name = "YouTube", checkboxIndex = 22))
+ exitMenu()
+ }
+ searchScreen {
+ clickSearchSelectorButton()
+ verifySearchShortcutListContains("Google", "Amazon.com", shouldExist = false)
+ verifySearchShortcutListContains("YouTube", shouldExist = true)
+ verifySearchShortcutListContains("Reddit", shouldExist = true)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSitePermissionsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSitePermissionsTest.kt
new file mode 100644
index 0000000000..4b3842fc6a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSitePermissionsTest.kt
@@ -0,0 +1,538 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.core.net.toUri
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.filters.SdkSuppress
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestAssetHelper.getMutedVideoPageAsset
+import org.mozilla.fenix.helpers.TestAssetHelper.getVideoPageAsset
+import org.mozilla.fenix.helpers.TestHelper.exitMenu
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying
+ * - site permissions settings sub-menu
+ * - the settings effects on the app behavior
+ *
+ */
+class SettingsSitePermissionsTest : TestSetup() {
+ /* Test page created and handled by the Mozilla mobile test-eng team */
+ private val permissionsTestPage = "https://mozilla-mobile.github.io/testapp/v2.0/permissions"
+ private val permissionsTestPageHost = "https://mozilla-mobile.github.io"
+ private val testPageSubstring = "https://mozilla-mobile.github.io:443"
+
+ @get:Rule
+ val activityTestRule = HomeActivityTestRule(
+ isJumpBackInCFREnabled = false,
+ isPWAsPromptEnabled = false,
+ isTCPCFREnabled = false,
+ isDeleteSitePermissionsEnabled = true,
+ )
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/246974
+ @Test
+ fun sitePermissionsItemsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ verifySitePermissionsToolbarTitle()
+ verifyToolbarGoBackButton()
+ verifySitePermissionOption("Autoplay", "Block audio only")
+ verifySitePermissionOption("Camera", "Blocked by Android")
+ verifySitePermissionOption("Location", "Blocked by Android")
+ verifySitePermissionOption("Microphone", "Blocked by Android")
+ verifySitePermissionOption("Notification", "Ask to allow")
+ verifySitePermissionOption("Persistent Storage", "Ask to allow")
+ verifySitePermissionOption("Cross-site cookies", "Ask to allow")
+ verifySitePermissionOption("DRM-controlled content", "Ask to allow")
+ verifySitePermissionOption("Exceptions")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/247680
+ // Verifies that you can go to System settings and change app's permissions from inside the app
+ @SmokeTest
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun systemBlockedPermissionsRedirectToSystemAppSettingsTest() {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openCamera {
+ verifyBlockedByAndroidSection()
+ }.goBack {
+ }.openLocation {
+ verifyBlockedByAndroidSection()
+ }.goBack {
+ }.openMicrophone {
+ verifyBlockedByAndroidSection()
+ clickGoToSettingsButton()
+ openAppSystemPermissionsSettings()
+ switchAppPermissionSystemSetting("Camera", "Allow")
+ goBackToSystemAppPermissionSettings()
+ verifySystemGrantedPermission("Camera")
+ switchAppPermissionSystemSetting("Location", "Allow")
+ goBackToSystemAppPermissionSettings()
+ verifySystemGrantedPermission("Location")
+ switchAppPermissionSystemSetting("Microphone", "Allow")
+ goBackToSystemAppPermissionSettings()
+ verifySystemGrantedPermission("Microphone")
+ goBackToPermissionsSettingsSubMenu()
+ verifyUnblockedByAndroid()
+ }.goBack {
+ }.openLocation {
+ verifyUnblockedByAndroid()
+ }.goBack {
+ }.openCamera {
+ verifyUnblockedByAndroid()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2095125
+ @SmokeTest
+ @Test
+ fun verifyAutoplayBlockAudioOnlySettingOnNotMutedVideoTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+ val videoTestPage = getVideoPageAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openAutoPlay {
+ verifySitePermissionsAutoPlaySubMenuItems()
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ verifyPageContent(genericPage.content)
+ }.openTabDrawer {
+ closeTab()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(videoTestPage.url) {
+ try {
+ verifyPageContent(videoTestPage.content)
+ clickPageObject(itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ } catch (e: java.lang.AssertionError) {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ verifyPageContent(videoTestPage.content)
+ clickPageObject(itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2286807
+ @Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1827599")
+ @SmokeTest
+ @Test
+ fun verifyAutoplayBlockAudioOnlySettingOnMutedVideoTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+ val mutedVideoTestPage = getMutedVideoPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ verifyPageContent(genericPage.content)
+ }.openTabDrawer {
+ closeTab()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(mutedVideoTestPage.url) {
+ try {
+ verifyPageContent("Media file is playing")
+ } catch (e: java.lang.AssertionError) {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ verifyPageContent("Media file is playing")
+ }
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2095124
+ @Test
+ fun verifyAutoplayAllowAudioVideoSettingOnNotMutedVideoTestTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+ val videoTestPage = getVideoPageAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openAutoPlay {
+ selectAutoplayOption("Allow audio and video")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericPage.url) {
+ verifyPageContent(genericPage.content)
+ }.openTabDrawer {
+ closeTab()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(videoTestPage.url) {
+ try {
+ verifyPageContent(videoTestPage.content)
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ } catch (e: java.lang.AssertionError) {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ verifyPageContent(videoTestPage.content)
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2286806
+ @Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1827599")
+ @Test
+ fun verifyAutoplayAllowAudioVideoSettingOnMutedVideoTest() {
+ val mutedVideoTestPage = getMutedVideoPageAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openAutoPlay {
+ selectAutoplayOption("Allow audio and video")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(mutedVideoTestPage.url) {
+ try {
+ verifyPageContent("Media file is playing")
+ } catch (e: java.lang.AssertionError) {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ verifyPageContent("Media file is playing")
+ }
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2095126
+ @Test
+ fun verifyAutoplayBlockAudioAndVideoSettingOnNotMutedVideoTest() {
+ val videoTestPage = getVideoPageAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openAutoPlay {
+ selectAutoplayOption("Block audio and video")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(videoTestPage.url) {
+ try {
+ verifyPageContent(videoTestPage.content)
+ clickPageObject(itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ } catch (e: java.lang.AssertionError) {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ verifyPageContent(videoTestPage.content)
+ clickPageObject(itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2286808
+ @Test
+ fun verifyAutoplayBlockAudioAndVideoSettingOnMutedVideoTest() {
+ val mutedVideoTestPage = getMutedVideoPageAsset(mockWebServer)
+
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openAutoPlay {
+ selectAutoplayOption("Block audio and video")
+ exitMenu()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(mutedVideoTestPage.url) {
+ verifyPageContent("Media file not playing")
+ clickPageObject(itemWithText("Play"))
+ try {
+ verifyPageContent("Media file is playing")
+ } catch (e: java.lang.AssertionError) {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ clickPageObject(itemWithText("Play"))
+ verifyPageContent("Media file is playing")
+ }
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/247362
+ @Test
+ fun verifyCameraPermissionSettingsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickStartCameraButton {
+ grantSystemPermission()
+ verifyCameraPermissionPrompt(testPageSubstring)
+ pressBack()
+ }
+ browserScreen {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openCamera {
+ verifySitePermissionsCommonSubMenuItems()
+ selectPermissionSettingOption("Blocked")
+ exitMenu()
+ }
+ }.clickStartCameraButton {}
+ browserScreen {
+ verifyPageContent("Camera not allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/247364
+ @Test
+ fun verifyMicrophonePermissionSettingsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickStartMicrophoneButton {
+ grantSystemPermission()
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ pressBack()
+ }
+ browserScreen {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openMicrophone {
+ verifySitePermissionsCommonSubMenuItems()
+ selectPermissionSettingOption("Blocked")
+ exitMenu()
+ }
+ }.clickStartMicrophoneButton {}
+ browserScreen {
+ verifyPageContent("Microphone not allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/247363
+ @Test
+ fun verifyLocationPermissionSettingsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickGetLocationButton {
+ verifyLocationPermissionPrompt(testPageSubstring)
+ pressBack()
+ }
+ browserScreen {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openLocation {
+ verifySitePermissionsCommonSubMenuItems()
+ selectPermissionSettingOption("Blocked")
+ exitMenu()
+ }
+ }.clickGetLocationButton {}
+ browserScreen {
+ verifyPageContent("User denied geolocation prompt")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/247365
+ @Test
+ fun verifyNotificationsPermissionSettingsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickOpenNotificationButton {
+ verifyNotificationsPermissionPrompt(testPageSubstring)
+ pressBack()
+ }
+ browserScreen {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openNotification {
+ verifyNotificationSubMenuItems()
+ selectPermissionSettingOption("Blocked")
+ exitMenu()
+ }
+ }.clickOpenNotificationButton {}
+ browserScreen {
+ verifyPageContent("Notifications not allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1923415
+ @Test
+ fun verifyPersistentStoragePermissionSettingsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickRequestPersistentStorageAccessButton {
+ verifyPersistentStoragePermissionPrompt(testPageSubstring)
+ pressBack()
+ }
+ browserScreen {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openPersistentStorage {
+ verifySitePermissionsPersistentStorageSubMenuItems()
+ selectPermissionSettingOption("Blocked")
+ exitMenu()
+ }
+ }.clickRequestPersistentStorageAccessButton {}
+ browserScreen {
+ verifyPageContent("Persistent storage permission denied")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1923417
+ @Test
+ fun verifyDRMControlledContentPermissionSettingsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickRequestDRMControlledContentAccessButton {
+ verifyDRMContentPermissionPrompt(testPageSubstring)
+ pressBack()
+ browserScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openDRMControlledContent {
+ verifyDRMControlledContentSubMenuItems()
+ selectDRMControlledContentPermissionSettingOption("Blocked")
+ exitMenu()
+ }
+ browserScreen {
+ }.clickRequestDRMControlledContentAccessButton {}
+ browserScreen {
+ verifyPageContent("DRM-controlled content not allowed")
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openDRMControlledContent {
+ selectDRMControlledContentPermissionSettingOption("Allowed")
+ exitMenu()
+ }
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }.clickRequestDRMControlledContentAccessButton {}
+ browserScreen {
+ verifyPageContent("DRM-controlled content allowed")
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/246976
+ @SmokeTest
+ @Test
+ fun clearAllSitePermissionsExceptionsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickOpenNotificationButton {
+ verifyNotificationsPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(true) {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openExceptions {
+ verifyExceptionCreated(permissionsTestPageHost, true)
+ clickClearPermissionsOnAllSites()
+ verifyClearPermissionsDialog()
+ clickCancel()
+ clickClearPermissionsOnAllSites()
+ clickOK()
+ verifyExceptionsEmptyList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/247007
+ @Test
+ fun addAndClearOneWebPagePermission() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickOpenNotificationButton {
+ verifyNotificationsPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(true) {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openExceptions {
+ verifyExceptionCreated(permissionsTestPageHost, true)
+ openSiteExceptionsDetails(permissionsTestPageHost)
+ clickClearPermissionsForOneSite()
+ verifyClearPermissionsForOneSiteDialog()
+ clickCancel()
+ clickClearPermissionsForOneSite()
+ clickOK()
+ verifyExceptionsEmptyList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/326477
+ @Test
+ fun clearIndividuallyAWebPagePermission() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(permissionsTestPage.toUri()) {
+ }.clickOpenNotificationButton {
+ verifyNotificationsPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(true) {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openSettingsSubMenuSitePermissions {
+ }.openExceptions {
+ verifyExceptionCreated(permissionsTestPageHost, true)
+ openSiteExceptionsDetails(permissionsTestPageHost)
+ verifyPermissionSettingSummary("Notification", "Allowed")
+ openChangePermissionSettingsMenu("Notification")
+ clickClearOnePermissionForOneSite()
+ verifyResetPermissionDefaultForThisSiteDialog()
+ clickOK()
+ pressBack()
+ verifyPermissionSettingSummary("Notification", "Ask to allow")
+ pressBack()
+ // This should be changed to false, when https://bugzilla.mozilla.org/show_bug.cgi?id=1826297 is fixed
+ verifyExceptionCreated(permissionsTestPageHost, true)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SitePermissionsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SitePermissionsTest.kt
new file mode 100644
index 0000000000..629d98f0f3
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SitePermissionsTest.kt
@@ -0,0 +1,327 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import android.Manifest
+import android.content.Context
+import android.hardware.camera2.CameraManager
+import android.media.AudioManager
+import android.os.Build
+import androidx.core.net.toUri
+import androidx.test.rule.GrantPermissionRule
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
+import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MockLocationUpdatesRule
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.appContext
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying site permissions prompts & functionality
+ *
+ */
+class SitePermissionsTest : TestSetup() {
+ /* Test page created and handled by the Mozilla mobile test-eng team */
+ private val testPage = "https://mozilla-mobile.github.io/testapp/permissions"
+ private val testPageSubstring = "https://mozilla-mobile.github.io:443"
+ private val cameraManager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ private val micManager = appContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule(
+ isJumpBackInCFREnabled = false,
+ isPWAsPromptEnabled = false,
+ isTCPCFREnabled = false,
+ isDeleteSitePermissionsEnabled = true,
+ )
+
+ @get:Rule
+ val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.CAMERA,
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ )
+
+ @get: Rule
+ val mockLocationUpdatesRule = MockLocationUpdatesRule()
+
+ @get: Rule
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334295
+ @SmokeTest
+ @Test
+ fun audioVideoPermissionWithoutRememberingTheDecisionTest() {
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartAudioVideoButton {
+ verifyAudioVideoPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Camera and Microphone not allowed")
+ }.clickStartAudioVideoButton {
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("Camera and Microphone allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334294
+ @Test
+ fun blockAudioVideoPermissionRememberingTheDecisionTest() {
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+ assumeTrue(micManager.microphones.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartAudioVideoButton {
+ verifyAudioVideoPermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Camera and Microphone not allowed")
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }.clickStartAudioVideoButton { }
+ browserScreen {
+ verifyPageContent("Camera and Microphone not allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251388
+ @Test
+ fun allowAudioVideoPermissionRememberingTheDecisionTest() {
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+ assumeTrue(micManager.microphones.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartAudioVideoButton {
+ verifyAudioVideoPermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("Camera and Microphone allowed")
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }.clickStartAudioVideoButton { }
+ browserScreen {
+ verifyPageContent("Camera and Microphone allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334189
+ @Test
+ fun microphonePermissionWithoutRememberingTheDecisionTest() {
+ assumeTrue(micManager.microphones.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton {
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Microphone not allowed")
+ }.clickStartMicrophoneButton {
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("Microphone allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334190
+ @Test
+ fun blockMicrophonePermissionRememberingTheDecisionTest() {
+ assumeTrue(micManager.microphones.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton {
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Microphone not allowed")
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton { }
+ browserScreen {
+ verifyPageContent("Microphone not allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251387
+ @Test
+ fun allowMicrophonePermissionRememberingTheDecisionTest() {
+ assumeTrue(micManager.microphones.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton {
+ verifyMicrophonePermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("Microphone allowed")
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }.clickStartMicrophoneButton { }
+ browserScreen {
+ verifyPageContent("Microphone allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334076
+ @Test
+ fun cameraPermissionWithoutRememberingDecisionTest() {
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartCameraButton {
+ verifyCameraPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Camera not allowed")
+ }.clickStartCameraButton {
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("Camera allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334077
+ @Test
+ fun blockCameraPermissionRememberingTheDecisionTest() {
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartCameraButton {
+ verifyCameraPermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Camera not allowed")
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }.clickStartCameraButton { }
+ browserScreen {
+ verifyPageContent("Camera not allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251386
+ @Test
+ fun allowCameraPermissionRememberingTheDecisionTest() {
+ assumeTrue(cameraManager.cameraIdList.isNotEmpty())
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ waitForPageToLoad()
+ }.clickStartCameraButton {
+ verifyCameraPermissionPrompt(testPageSubstring)
+ selectRememberPermissionDecision()
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("Camera allowed")
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }.clickStartCameraButton { }
+ browserScreen {
+ verifyPageContent("Camera allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334074
+ @SmokeTest
+ @Test
+ fun blockNotificationsPermissionTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ }.clickOpenNotificationButton {
+ verifyNotificationsPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("Notifications not allowed")
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }.clickOpenNotificationButton {
+ verifyNotificationsPermissionPrompt(testPageSubstring, true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251380
+ @Test
+ fun allowNotificationsPermissionTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ }.clickOpenNotificationButton {
+ verifyNotificationsPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("Notifications allowed")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251385
+ @SmokeTest
+ @Test
+ fun allowLocationPermissionsTest() {
+ mockLocationUpdatesRule.setMockLocation()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ }.clickGetLocationButton {
+ verifyLocationPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(true) {
+ verifyPageContent("${mockLocationUpdatesRule.latitude}")
+ verifyPageContent("${mockLocationUpdatesRule.longitude}")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2334075
+ @Test
+ fun blockLocationPermissionsTest() {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.toUri()) {
+ }.clickGetLocationButton {
+ verifyLocationPermissionPrompt(testPageSubstring)
+ }.clickPagePermissionButton(false) {
+ verifyPageContent("User denied geolocation prompt")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2121537
+ @SmokeTest
+ @Test
+ fun fileUploadPermissionTest() {
+ val testPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(testPage.url) {
+ clickPageObject(itemWithResId("upload_file"))
+ grantSystemPermission()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ assertExternalAppOpens("com.google.android.documentsui")
+ } else {
+ assertExternalAppOpens("com.android.documentsui")
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SponsoredShortcutsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SponsoredShortcutsTest.kt
new file mode 100644
index 0000000000..47f6aff3f0
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SponsoredShortcutsTest.kt
@@ -0,0 +1,212 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.Constants.defaultTopSitesList
+import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredShortcutTitle
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+
+/**
+ * Tests Sponsored shortcuts functionality
+ */
+
+class SponsoredShortcutsTest : TestSetup() {
+ private lateinit var sponsoredShortcutTitle: String
+ private lateinit var sponsoredShortcutTitle2: String
+
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729331
+ // Expected for en-us defaults
+ @SmokeTest
+ @Test
+ fun verifySponsoredShortcutsListTest() {
+ homeScreen {
+ defaultTopSitesList.values.forEach { value ->
+ verifyExistingTopSitesTabs(value)
+ }
+ }.openThreeDotMenu {
+ }.openCustomizeHome {
+ verifySponsoredShortcutsCheckBox(true)
+ clickSponsoredShortcuts()
+ verifySponsoredShortcutsCheckBox(false)
+ }.goBackToHomeScreen {
+ verifyNotExistingSponsoredTopSitesList()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729338
+ @Test
+ fun openSponsoredShortcutTest() {
+ homeScreen {
+ sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
+ }.openSponsoredShortcut(sponsoredShortcutTitle) {
+ verifyUrl(sponsoredShortcutTitle)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729334
+ @Test
+ fun openSponsoredShortcutInPrivateTabTest() {
+ homeScreen {
+ sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
+ }.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
+ }.openTopSiteInPrivateTab {
+ verifyUrl(sponsoredShortcutTitle)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729335
+ @Test
+ fun openSponsorsAndYourPrivacyOptionTest() {
+ homeScreen {
+ sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
+ }.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
+ }.clickSponsorsAndPrivacyButton {
+ verifyUrl("support.mozilla.org/en-US/kb/sponsor-privacy")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729336
+ @Test
+ fun openSponsoredShortcutsSettingsOptionTest() {
+ homeScreen {
+ sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
+ }.openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle) {
+ }.clickSponsoredShortcutsSettingsButton {
+ verifyHomePageView()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729337
+ @Test
+ fun verifySponsoredShortcutsDetailsTest() {
+ homeScreen {
+ sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
+ sponsoredShortcutTitle2 = getSponsoredShortcutTitle(3)
+
+ verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
+ verifySponsoredShortcutDetails(sponsoredShortcutTitle2, 3)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729328
+ // 1 sponsored shortcut should be displayed if there are 7 pinned top sites
+ @Test
+ fun verifySponsoredShortcutsListWithSevenPinnedSitesTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+ val fourthWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
+
+ homeScreen {
+ sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
+ sponsoredShortcutTitle2 = getSponsoredShortcutTitle(3)
+
+ verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
+ verifySponsoredShortcutDetails(sponsoredShortcutTitle2, 3)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ verifyPageContent(firstWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifyExistingTopSitesTabs(firstWebPage.title)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifyExistingTopSitesTabs(secondWebPage.title)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(thirdWebPage.url) {
+ verifyPageContent(thirdWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifyExistingTopSitesTabs(thirdWebPage.title)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(fourthWebPage.url) {
+ verifyPageContent(fourthWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
+ verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle2, 3)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729329
+ // No sponsored shortcuts should be displayed if there are 8 pinned top sites
+ @Test
+ fun verifySponsoredShortcutsListWithEightPinnedSitesTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
+ val fourthWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
+ val fifthWebPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer)
+
+ homeScreen {
+ sponsoredShortcutTitle = getSponsoredShortcutTitle(2)
+ sponsoredShortcutTitle2 = getSponsoredShortcutTitle(3)
+
+ verifySponsoredShortcutDetails(sponsoredShortcutTitle, 2)
+ verifySponsoredShortcutDetails(sponsoredShortcutTitle2, 3)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ verifyPageContent(firstWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifyExistingTopSitesTabs(firstWebPage.title)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ verifyPageContent(secondWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifyExistingTopSitesTabs(secondWebPage.title)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(thirdWebPage.url) {
+ verifyPageContent(thirdWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifyExistingTopSitesTabs(thirdWebPage.title)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(fourthWebPage.url) {
+ verifyPageContent(fourthWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifyExistingTopSitesTabs(fourthWebPage.title)
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(fifthWebPage.url) {
+ verifyPageContent(fifthWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ }.addToFirefoxHome {
+ }.goToHomescreen {
+ verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle, 2)
+ verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle2, 3)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt
new file mode 100644
index 0000000000..062a998ecd
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt
@@ -0,0 +1,516 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.closeApp
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.notificationShade
+
+/**
+ * Tests for verifying basic functionality of tabbed browsing
+ *
+ * Including:
+ * - Opening a tab
+ * - Opening a private tab
+ * - Verifying tab list
+ * - Closing all tabs
+ * - Close tab
+ * - Swipe to close tab (temporarily disabled)
+ * - Undo close tab
+ * - Close private tabs persistent notification
+ * - Empty tab tray state
+ * - Tab tray details
+ * - Shortcut context menu navigation
+ */
+
+class TabbedBrowsingTest : TestSetup() {
+ @get:Rule
+ val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903599
+ @Test
+ fun closeAllTabsTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openTabDrawer {
+ verifyExistingTabList()
+ }.openTabsListThreeDotMenu {
+ verifyCloseAllTabsButton()
+ verifyShareTabButton()
+ verifySelectTabs()
+ }.closeAllTabs {
+ verifyTabCounter("0")
+ }
+
+ // Repeat for Private Tabs
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.openTabDrawer {
+ verifyPrivateModeSelected()
+ verifyExistingTabList()
+ }.openTabsListThreeDotMenu {
+ verifyCloseAllTabsButton()
+ }.closeAllTabs {
+ verifyTabCounter("0")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2349580
+ @Test
+ fun closingTabsTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openTabDrawer {
+ verifyExistingOpenTabs("Test_Page_1")
+ closeTab()
+ verifySnackBarText("Tab closed")
+ clickSnackbarButton("UNDO")
+ }
+ browserScreen {
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903604
+ @Test
+ fun swipeToCloseTabsTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ verifyExistingOpenTabs("Test_Page_1")
+ swipeTabRight("Test_Page_1")
+ verifySnackBarText("Tab closed")
+ }
+ homeScreen {
+ verifyTabCounter("0")
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ waitForPageToLoad()
+ }.openTabDrawer {
+ verifyExistingOpenTabs("Test_Page_1")
+ swipeTabLeft("Test_Page_1")
+ verifySnackBarText("Tab closed")
+ }
+ homeScreen {
+ verifyTabCounter("0")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903591
+ @Test
+ fun closingPrivateTabsTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen { }.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openTabDrawer {
+ verifyExistingOpenTabs("Test_Page_1")
+ verifyCloseTabsButton("Test_Page_1")
+ closeTab()
+ verifySnackBarText("Private tab closed")
+ clickSnackbarButton("UNDO")
+ }
+ browserScreen {
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903606
+ @SmokeTest
+ @Test
+ fun tabMediaControlButtonTest() {
+ val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(audioTestPage.url) {
+ mDevice.waitForIdle()
+ clickPageObject(MatcherHelper.itemWithText("Play"))
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
+ }.openTabDrawer {
+ verifyTabMediaControlButtonState("Pause")
+ clickTabMediaControlButton("Pause")
+ verifyTabMediaControlButtonState("Play")
+ }.openTab(audioTestPage.title) {
+ assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903592
+ @SmokeTest
+ @Test
+ fun verifyCloseAllPrivateTabsNotificationTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ mDevice.openNotification()
+ }
+
+ notificationShade {
+ verifyPrivateTabsNotification()
+ }.clickClosePrivateTabsNotification {
+ verifyHomeScreen()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903598
+ @SmokeTest
+ @Test
+ fun shareTabsFromTabsTrayTest() {
+ val firstWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+ val firstWebsiteTitle = firstWebsite.title
+ val secondWebsiteTitle = secondWebsite.title
+ val sharingApp = "Gmail"
+ val sharedUrlsString = "${firstWebsite.url}\n\n${secondWebsite.url}"
+
+ homeScreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebsite.url) {
+ verifyPageContent(firstWebsite.content)
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebsite.url.toString()) {
+ verifyPageContent(secondWebsite.content)
+ }.openTabDrawer {
+ verifyExistingOpenTabs("Test_Page_1")
+ verifyExistingOpenTabs("Test_Page_2")
+ }.openTabsListThreeDotMenu {
+ verifyShareAllTabsButton()
+ }.clickShareAllTabsButton {
+ verifyShareTabsOverlay(firstWebsiteTitle, secondWebsiteTitle)
+ verifySharingWithSelectedApp(
+ sharingApp,
+ sharedUrlsString,
+ "$firstWebsiteTitle, $secondWebsiteTitle",
+ )
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903602
+ @Test
+ fun verifyTabTrayNotShowingStateHalfExpanded() {
+ navigationToolbar {
+ }.openTabTray {
+ verifyNoOpenTabsInNormalBrowsing()
+ // With no tabs opened the state should be STATE_COLLAPSED.
+ verifyBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
+ // Need to ensure the halfExpandedRatio is very small so that when in STATE_HALF_EXPANDED
+ // the tabTray will actually have a very small height (for a very short time) akin to being hidden.
+ verifyHalfExpandedRatio()
+ }.clickTopBar {
+ }.waitForTabTrayBehaviorToIdle {
+ // Touching the topBar would normally advance the tabTray to the next state.
+ // We don't want that.
+ verifyBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
+ }.advanceToHalfExpandedState {
+ }.waitForTabTrayBehaviorToIdle {
+ // TabTray should not be displayed in STATE_HALF_EXPANDED.
+ // When advancing to this state it should immediately be hidden.
+ verifyTabTrayIsClosed()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903600
+ @Test
+ fun verifyEmptyTabTray() {
+ navigationToolbar {
+ }.openTabTray {
+ verifyNormalBrowsingButtonIsSelected(true)
+ verifyPrivateBrowsingButtonIsSelected(false)
+ verifySyncedTabsButtonIsSelected(false)
+ verifyNoOpenTabsInNormalBrowsing()
+ verifyNormalBrowsingNewTabButton()
+ verifyTabTrayOverflowMenu(true)
+ verifyEmptyTabsTrayMenuButtons()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903585
+ @Test
+ fun verifyEmptyPrivateTabsTrayTest() {
+ navigationToolbar {
+ }.openTabTray {
+ }.toggleToPrivateTabs {
+ verifyNormalBrowsingButtonIsSelected(false)
+ verifyPrivateBrowsingButtonIsSelected(true)
+ verifySyncedTabsButtonIsSelected(false)
+ verifyNoOpenTabsInPrivateBrowsing()
+ verifyPrivateBrowsingNewTabButton()
+ verifyTabTrayOverflowMenu(true)
+ verifyEmptyTabsTrayMenuButtons()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903601
+ @Test
+ fun verifyTabsTrayWithOpenTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(defaultWebPage.url.toString()) {
+ }.openTabDrawer {
+ verifyNormalBrowsingButtonIsSelected(true)
+ verifyPrivateBrowsingButtonIsSelected(false)
+ verifySyncedTabsButtonIsSelected(false)
+ verifyTabTrayOverflowMenu(true)
+ verifyTabsTrayCounter()
+ verifyExistingTabList()
+ verifyNormalBrowsingNewTabButton()
+ verifyOpenedTabThumbnail()
+ verifyExistingOpenTabs(defaultWebPage.title)
+ verifyCloseTabsButton(defaultWebPage.title)
+ }.openTab(defaultWebPage.title) {
+ verifyUrl(defaultWebPage.url.toString())
+ verifyTabCounter("1")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903587
+ @SmokeTest
+ @Test
+ fun verifyPrivateTabsTrayWithOpenTabTest() {
+ val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.openTabDrawer {
+ }.toggleToPrivateTabs {
+ }.openNewTab {
+ }.submitQuery(website.url.toString()) {
+ }.openTabDrawer {
+ verifyNormalBrowsingButtonIsSelected(false)
+ verifyPrivateBrowsingButtonIsSelected(true)
+ verifySyncedTabsButtonIsSelected(false)
+ verifyTabTrayOverflowMenu(true)
+ verifyTabsTrayCounter()
+ verifyExistingTabList()
+ verifyExistingOpenTabs(website.title)
+ verifyCloseTabsButton(website.title)
+ verifyOpenedTabThumbnail()
+ verifyPrivateBrowsingNewTabButton()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927314
+ @Test
+ fun tabsCounterShortcutMenuCloseTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ waitForPageToLoad()
+ }.goToHomescreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ waitForPageToLoad()
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ verifyTabButtonShortcutMenuItems()
+ }.closeTabFromShortcutsMenu {
+ browserScreen {
+ verifyTabCounter("1")
+ verifyPageContent(firstWebPage.content)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343663
+ @Test
+ fun tabsCounterShortcutMenuNewPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {}
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewPrivateTabFromShortcutsMenu {
+ verifySearchBarPlaceholder("Search or enter address")
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343662
+ @Test
+ fun tabsCounterShortcutMenuNewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {}
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewTabFromShortcutsMenu {
+ verifySearchBarPlaceholder("Search or enter address")
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927315
+ @Test
+ fun privateTabsCounterShortcutMenuCloseTabTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ waitForPageToLoad()
+ }.goToHomescreen {
+ }.openNavigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebPage.url) {
+ waitForPageToLoad()
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ verifyTabButtonShortcutMenuItems()
+ }.closeTabFromShortcutsMenu {
+ browserScreen {
+ verifyTabCounter("1")
+ verifyPageContent(firstWebPage.content)
+ }
+ }.openTabButtonShortcutsMenu {
+ }.closeTabFromShortcutsMenu {
+ homeScreen {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344199
+ @Test
+ fun privateTabsCounterShortcutMenuNewPrivateTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewPrivateTabFromShortcutsMenu {
+ verifySearchBarPlaceholder("Search or enter address")
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344198
+ @Test
+ fun privateTabsCounterShortcutMenuNewTabTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true)
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }
+ navigationToolbar {
+ }.openTabButtonShortcutsMenu {
+ }.openNewTabFromShortcutsMenu {
+ verifySearchToolbar(isDisplayed = true)
+ }.dismissSearchBar {
+ verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1046683
+ @Test
+ fun verifySyncedTabsWhenUserIsNotSignedInTest() {
+ navigationToolbar {
+ }.openTabTray {
+ verifySyncedTabsButtonIsSelected(isSelected = false)
+ clickSyncedTabsButton()
+ }.toggleToSyncedTabs {
+ verifySyncedTabsButtonIsSelected(isSelected = true)
+ verifySyncedTabsListWhenUserIsNotSignedIn()
+ }.clickSignInToSyncButton {
+ verifyTurnOnSyncMenu()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/526244
+ @Test
+ fun privateModeStaysAsDefaultAfterRestartTest() {
+ val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ }.goToHomescreen {
+ }.togglePrivateBrowsingMode()
+ closeApp(activityTestRule)
+ restartApp(activityTestRule)
+ homeScreen {
+ verifyPrivateBrowsingHomeScreenItems()
+ }.openTabDrawer {
+ }.toggleToNormalTabs {
+ verifyExistingOpenTabs(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2228470
+ @SmokeTest
+ @Test
+ fun privateTabsDoNotPersistAfterClosingAppTest() {
+ val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebPage.url) {
+ }.openTabDrawer {
+ }.openNewTab {
+ }.submitQuery(secondWebPage.url.toString()) {}
+ closeApp(activityTestRule)
+ restartApp(activityTestRule)
+ homeScreen {
+ verifyPrivateBrowsingHomeScreenItems()
+ }.openTabDrawer {
+ verifyNoOpenTabsInPrivateBrowsing()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TextSelectionTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TextSelectionTest.kt
new file mode 100644
index 0000000000..2cf731d63c
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TextSelectionTest.kt
@@ -0,0 +1,319 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.RetryTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.clickContextMenuItem
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.longClickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import org.mozilla.fenix.ui.robots.openEditURLView
+import org.mozilla.fenix.ui.robots.searchScreen
+import org.mozilla.fenix.ui.robots.shareOverlay
+
+class TextSelectionTest : TestSetup() {
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
+
+ @Rule
+ @JvmField
+ val retryTestRule = RetryTestRule(3)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326832
+ @SmokeTest
+ @Test
+ fun verifySelectAllTextOptionTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ longClickPageObject(itemContainingText("content"))
+ clickContextMenuItem("Select all")
+ clickContextMenuItem("Copy")
+ }.openNavigationToolbar {
+ openEditURLView()
+ }
+
+ searchScreen {
+ clickClearButton()
+ longClickToolbar()
+ clickPasteText()
+ // With Select all, white spaces are copied
+ // Potential bug https://bugzilla.mozilla.org/show_bug.cgi?id=1821310
+ verifyTypedToolbarText(" Page content: 1 ")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326828
+ @Test
+ fun verifyCopyTextOptionTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ longClickPageObject(itemContainingText("content"))
+ clickContextMenuItem("Copy")
+ }.openNavigationToolbar {
+ openEditURLView()
+ }
+
+ searchScreen {
+ clickClearButton()
+ longClickToolbar()
+ clickPasteText()
+ verifyTypedToolbarText("content")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326829
+ @Test
+ fun verifyShareSelectedTextOptionTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ longClickPageObject(itemWithText(genericURL.content))
+ }.clickShareSelectedText {
+ verifyAndroidShareLayout()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326830
+ @Test
+ fun verifySearchTextOptionTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ longClickPageObject(itemContainingText("content"))
+ clickContextMenuItem("Search")
+ mDevice.waitForIdle()
+ verifyTabCounter("2")
+ verifyUrl("content")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326831
+ @SmokeTest
+ @Test
+ fun verifyPrivateSearchTextTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ longClickPageObject(itemContainingText("content"))
+ clickContextMenuItem("Private Search")
+ mDevice.waitForIdle()
+ verifyTabCounter("2")
+ verifyUrl("content")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326834
+ @Test
+ fun verifySelectAllPDFTextOptionTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ longClickPageObject(itemContainingText("Crossing"))
+ clickContextMenuItem("Select all")
+ clickContextMenuItem("Copy")
+ }.openNavigationToolbar {
+ openEditURLView()
+ }
+
+ searchScreen {
+ clickClearButton()
+ longClickToolbar()
+ clickPasteText()
+ verifyTypedToolbarText("Washington Crossing the Delaware Wikipedia linkName: Android")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243839
+ @SmokeTest
+ @Test
+ fun verifyCopyPDFTextOptionTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ longClickPageObject(itemContainingText("Crossing"))
+ clickContextMenuItem("Copy")
+ }.openNavigationToolbar {
+ openEditURLView()
+ }
+
+ searchScreen {
+ clickClearButton()
+ longClickToolbar()
+ clickPasteText()
+ verifyTypedToolbarText("Crossing")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326835
+ @Test
+ fun verifyShareSelectedPDFTextOptionTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ longClickPageObject(itemContainingText("Crossing"))
+ }.clickShareSelectedText {
+ verifyAndroidShareLayout()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326836
+ @SmokeTest
+ @Test
+ fun verifySearchPDFTextOptionTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ longClickPageObject(itemContainingText("Crossing"))
+ clickContextMenuItem("Search")
+ verifyTabCounter("2")
+ verifyUrl("Crossing")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326837
+ @Test
+ fun verifyPrivateSearchPDFTextOptionTest() {
+ val genericURL =
+ TestAssetHelper.getGenericAsset(mockWebServer, 3)
+
+ homeScreen {
+ }.togglePrivateBrowsingMode()
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ clickPageObject(itemWithText("PDF form file"))
+ longClickPageObject(itemContainingText("Crossing"))
+ clickContextMenuItem("Private Search")
+ verifyTabCounter("2")
+ verifyUrl("Crossing")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326813
+ @Test
+ fun verifyUrlBarTextSelectionOptionsTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openNavigationToolbar {
+ longClickEditModeToolbar()
+ verifyTextSelectionOptions("Open", "Cut", "Copy", "Share")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326814
+ @Test
+ fun verifyCopyUrlBarTextSelectionOptionTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openNavigationToolbar {
+ longClickEditModeToolbar()
+ clickContextMenuItem("Copy")
+ clickClearToolbarButton()
+ verifyToolbarIsEmpty()
+ longClickEditModeToolbar()
+ clickContextMenuItem("Paste")
+ verifyUrl(genericURL.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2326815
+ @Test
+ fun verifyCutUrlBarTextSelectionOptionTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openNavigationToolbar {
+ longClickEditModeToolbar()
+ clickContextMenuItem("Cut")
+ verifyToolbarIsEmpty()
+ longClickEditModeToolbar()
+ clickContextMenuItem("Paste")
+ verifyUrl(genericURL.url.toString())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243845
+ @SmokeTest
+ @Test
+ fun verifyShareUrlBarTextSelectionOptionTest() {
+ val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(genericURL.url) {
+ }.openNavigationToolbar {
+ longClickEditModeToolbar()
+ clickContextMenuItem("Share")
+ }
+ shareOverlay {
+ verifyAndroidShareLayout()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414316
+ @Test
+ fun urlBarQuickActionsTest() {
+ val firstWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 1)
+ val secondWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 2)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(firstWebsite.url) {
+ longClickToolbar()
+ clickContextMenuItem("Copy")
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(secondWebsite.url) {
+ longClickToolbar()
+ clickContextMenuItem("Paste")
+ }
+ searchScreen {
+ verifyTypedToolbarText(firstWebsite.url.toString())
+ }.dismissSearchBar {
+ }
+ browserScreen {
+ verifyUrl(secondWebsite.url.toString())
+ longClickToolbar()
+ clickContextMenuItem("Paste & Go")
+ verifyUrl(firstWebsite.url.toString())
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt
new file mode 100644
index 0000000000..a5426c33ea
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt
@@ -0,0 +1,244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.customannotations.SmokeTest
+import org.mozilla.fenix.helpers.Constants.defaultTopSitesList
+import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
+import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.browserScreen
+import org.mozilla.fenix.ui.robots.homeScreen
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests Top Sites functionality
+ *
+ * - Verifies 'Add to Firefox Home' UI functionality
+ * - Verifies 'Top Sites' context menu UI functionality
+ * - Verifies 'Top Site' usage UI functionality
+ * - Verifies existence of default top sites available on the home-screen
+ */
+
+class TopSitesTest : TestSetup() {
+ @get:Rule
+ val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532598
+ @SmokeTest
+ @Test
+ fun addAWebsiteAsATopSiteTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(shouldExist = true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532599
+ @Test
+ fun openTopSiteInANewTabTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(shouldExist = true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openTopSiteTabWithTitle(title = defaultWebPage.title) {
+ verifyUrl(defaultWebPage.url.toString().replace("http://", ""))
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }
+
+ // Dismiss context menu popup
+ mDevice.pressBack()
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532600
+ @Test
+ fun openTopSiteInANewPrivateTabTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(shouldExist = true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.openTopSiteInPrivateTab {
+ verifyCurrentPrivateSession(activityIntentTestRule.activity.applicationContext)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1110321
+ @Test
+ fun renameATopSiteTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+ val newPageTitle = generateRandomString(5)
+
+ homeScreen {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(shouldExist = true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.renameTopSite(newPageTitle) {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(newPageTitle)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532601
+ @Test
+ fun removeTopSiteUsingMenuButtonTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(shouldExist = true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.removeTopSite {
+ clickSnackbarButton("UNDO")
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ verifyTopSiteContextMenuItems()
+ }.removeTopSite {
+ verifyNotExistingTopSitesList(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2323641
+ @Test
+ fun removeTopSiteFromMainMenuTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ homeScreen {
+ verifyExistingTopSitesList()
+ }
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ verifyPageContent(defaultWebPage.content)
+ }.openThreeDotMenu {
+ expandMenu()
+ verifyAddToShortcutsButton(shouldExist = true)
+ }.addToFirefoxHome {
+ verifySnackBarText(getStringResource(R.string.snackbar_added_to_shortcuts))
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openTopSiteTabWithTitle(defaultWebPage.title) {
+ }.openThreeDotMenu {
+ verifyRemoveFromShortcutsButton()
+ }.clickRemoveFromShortcuts {
+ }.goToHomescreen {
+ verifyNotExistingTopSitesList(defaultWebPage.title)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561582
+ // Expected for en-us defaults
+ @Test
+ fun verifyENLocalesDefaultTopSitesListTest() {
+ homeScreen {
+ verifyExistingTopSitesList()
+ defaultTopSitesList.values.forEach { value ->
+ verifyExistingTopSitesTabs(value)
+ }
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1050642
+ @SmokeTest
+ @Test
+ fun addAndRemoveMostViewedTopSiteTest() {
+ val defaultWebPage = getGenericAsset(mockWebServer, 1)
+
+ for (i in 0..1) {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(defaultWebPage.url) {
+ waitForPageToLoad()
+ }
+ }
+
+ browserScreen {
+ }.goToHomescreen {
+ verifyExistingTopSitesList()
+ verifyExistingTopSitesTabs(defaultWebPage.title)
+ }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
+ }.deleteTopSiteFromHistory {
+ verifySnackBarText(getStringResource(R.string.snackbar_top_site_removed))
+ waitUntilSnackbarGone()
+ }.openThreeDotMenu {
+ }.openHistory {
+ verifyEmptyHistoryView()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TotalCookieProtectionTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TotalCookieProtectionTest.kt
new file mode 100644
index 0000000000..9e708b2de9
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/TotalCookieProtectionTest.kt
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.components.toolbar.CFR_MINIMUM_NUMBER_OPENED_TABS
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.navigationToolbar
+
+/**
+ * Tests for verifying the new Cookie protection & homescreen feature hints.
+ * Note: This involves setting the feature flags On for CFRs which are disabled elsewhere.
+ *
+ */
+class TotalCookieProtectionTest : TestSetup() {
+ @get:Rule
+ val composeTestRule = AndroidComposeTestRule(
+ HomeActivityTestRule(
+ isTCPCFREnabled = true,
+ ),
+ ) { it.activity }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ CFR_MINIMUM_NUMBER_OPENED_TABS = 0
+ }
+
+ @After
+ override fun tearDown() {
+ super.tearDown()
+ CFR_MINIMUM_NUMBER_OPENED_TABS = 5
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2260552
+ @Test
+ fun openTotalCookieProtectionLearnMoreLinkTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowserForTCPCFR(genericPage.url) {
+ waitForPageToLoad()
+ verifyCookiesProtectionHintIsDisplayed(composeTestRule, true)
+ clickTCPCFRLearnMore(composeTestRule)
+ verifyUrl("support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-android")
+ verifyShouldShowCFRTCP(false, composeTestRule.activity.settings())
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1913589
+ @Test
+ fun dismissTotalCookieProtectionHintTest() {
+ val genericPage = getGenericAsset(mockWebServer, 1)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowserForTCPCFR(genericPage.url) {
+ waitForPageToLoad()
+ verifyCookiesProtectionHintIsDisplayed(composeTestRule, true)
+ dismissTCPCFRPopup(composeTestRule)
+ verifyCookiesProtectionHintIsDisplayed(composeTestRule, false)
+ verifyShouldShowCFRTCP(false, composeTestRule.activity.settings())
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/UpgradingUsersOnboardingTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/UpgradingUsersOnboardingTest.kt
new file mode 100644
index 0000000000..04f435c1ca
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/UpgradingUsersOnboardingTest.kt
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.relaunchCleanApp
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.homeScreen
+
+/**
+ * Tests for verifying the onboarding feature for users upgrading from a version older than 106.
+ * Note: This involves setting the feature flag On for the onboarding cards
+ *
+ */
+class UpgradingUsersOnboardingTest : TestSetup() {
+
+ @get:Rule
+ val activityTestRule = AndroidComposeTestRule(
+ HomeActivityIntentTestRule(isHomeOnboardingDialogEnabled = true),
+ ) { it.activity }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1913592
+ @Test
+ fun upgradingUsersOnboardingScreensTest() {
+ homeScreen {
+ verifyUpgradingUserOnboardingFirstScreen(activityTestRule)
+ clickGetStartedButton(activityTestRule)
+ verifyUpgradingUserOnboardingSecondScreen(activityTestRule)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1913591
+ @Test
+ fun upgradingUsersOnboardingCanBeSkippedTest() {
+ homeScreen {
+ verifyUpgradingUserOnboardingFirstScreen(activityTestRule)
+ clickCloseButton(activityTestRule)
+ verifyHomeScreen()
+
+ relaunchCleanApp(activityTestRule.activityRule)
+ clickGetStartedButton(activityTestRule)
+ verifyUpgradingUserOnboardingSecondScreen(activityTestRule)
+ clickSkipButton(activityTestRule)
+ verifyHomeScreen()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1932156
+ @Test
+ fun upgradingUsersOnboardingSignInButtonTest() {
+ homeScreen {
+ verifyUpgradingUserOnboardingFirstScreen(activityTestRule)
+ clickGetStartedButton(activityTestRule)
+ verifyUpgradingUserOnboardingSecondScreen(activityTestRule)
+ }.clickUpgradingUserOnboardingSignInButton(activityTestRule) {
+ verifyTurnOnSyncMenu()
+ mDevice.pressBack()
+ }
+ homeScreen {
+ verifyHomeScreen()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt
new file mode 100644
index 0000000000..2fea99511a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui
+
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.HomeActivityTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestAssetHelper.getHTMLControlsFormAsset
+import org.mozilla.fenix.helpers.TestSetup
+import org.mozilla.fenix.ui.robots.clickPageObject
+import org.mozilla.fenix.ui.robots.navigationToolbar
+import java.time.LocalDate
+
+/**
+ * Tests for verifying basic interactions with web control elements
+ *
+ */
+
+class WebControlsTest : TestSetup() {
+ private val hour = 10
+ private val minute = 10
+ private val colorHexValue = "#5b2067"
+ private val emailLink = "mailto://example@example.com"
+ private val phoneLink = "tel://1234567890"
+
+ @get:Rule
+ val activityTestRule = HomeActivityTestRule(
+ isJumpBackInCFREnabled = false,
+ isTCPCFREnabled = false,
+ )
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2316067
+ @Test
+ fun verifyCalendarFormInteractionsTest() {
+ val currentDate = LocalDate.now()
+ val currentDay = currentDate.dayOfMonth
+ val currentMonth = currentDate.month
+ val currentYear = currentDate.year
+ val htmlControlsPage = getHTMLControlsFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(htmlControlsPage.url) {
+ clickPageObject(itemWithResId("calendar"))
+ clickPageObject(itemContainingText("CANCEL"))
+ clickPageObject(itemWithResId("submitDate"))
+ verifyNoDateIsSelected()
+ clickPageObject(itemWithResId("calendar"))
+ clickPageObject(itemWithDescription("$currentDay $currentMonth $currentYear"))
+ clickPageObject(itemContainingText("OK"))
+ clickPageObject(itemWithResId("submitDate"))
+ verifySelectedDate()
+ clickPageObject(itemWithResId("calendar"))
+ clickPageObject(itemContainingText("CLEAR"))
+ clickPageObject(itemWithResId("submitDate"))
+ verifyNoDateIsSelected()
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2316069
+ @Test
+ fun verifyClockFormInteractionsTest() {
+ val htmlControlsPage = getHTMLControlsFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(htmlControlsPage.url) {
+ clickPageObject(itemWithResId("clock"))
+ clickPageObject(itemContainingText("CANCEL"))
+ clickPageObject(itemWithResId("submitTime"))
+ verifyNoTimeIsSelected(hour, minute)
+ clickPageObject(itemWithResId("clock"))
+ selectTime(hour, minute)
+ clickPageObject(itemContainingText("OK"))
+ clickPageObject(itemWithResId("submitTime"))
+ verifySelectedTime(hour, minute)
+ clickPageObject(itemWithResId("clock"))
+ clickPageObject(itemContainingText("CLEAR"))
+ clickPageObject(itemWithResId("submitTime"))
+ verifyNoTimeIsSelected(hour, minute)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2316068
+ @Test
+ fun verifyColorPickerInteractionsTest() {
+ val htmlControlsPage = getHTMLControlsFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(htmlControlsPage.url) {
+ clickPageObject(itemWithResId("colorPicker"))
+ clickPageObject(itemWithDescription(colorHexValue))
+ clickPageObject(itemContainingText("CANCEL"))
+ clickPageObject(itemWithResId("submitColor"))
+ verifyColorIsNotSelected(colorHexValue)
+ clickPageObject(itemWithResId("colorPicker"))
+ clickPageObject(itemWithDescription(colorHexValue))
+ clickPageObject(itemContainingText("SET"))
+ clickPageObject(itemWithResId("submitColor"))
+ verifySelectedColor(colorHexValue)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2316070
+ @Test
+ fun verifyDropdownMenuInteractionsTest() {
+ val htmlControlsPage = getHTMLControlsFormAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(htmlControlsPage.url) {
+ clickPageObject(itemWithResId("dropDown"))
+ clickPageObject(itemContainingText("The National"))
+ clickPageObject(itemWithResId("submitOption"))
+ verifySelectedDropDownOption("The National")
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2316071
+ @Test
+ fun verifyEmailLinkTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(itemContainingText("Email link"))
+ clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
+ assertNativeAppOpens(Constants.PackageName.GMAIL_APP, emailLink)
+ }
+ }
+
+ // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/834205
+ @Test
+ fun verifyTelephoneLinkTest() {
+ val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
+
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(externalLinksPage.url) {
+ clickPageObject(itemContainingText("Telephone link"))
+ clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
+ assertNativeAppOpens(Constants.PackageName.PHONE_APP, phoneLink)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/docs/assets.md b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/docs/assets.md
new file mode 100644
index 0000000000..1aa5578ecc
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/docs/assets.md
@@ -0,0 +1,33 @@
+# README
+_This document is intended to explain specifics in regards to test assets for the Espresso/UI Automator Android UI tests._
+
+# Test application
+* Remote test page managed by Mozilla Mobile Test Engineering at https://github.com/mozilla-mobile/testapp
+* File type download page: https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html
+
+# Assets
+
+Currently, Espresso/UI Automator UI tests use remote assets located on a Google Cloud Storage bucket at: [mobile_test_assets](https://storage.googleapis.com/mobile_test_assets)
+
+|Filetype|URL|
+|----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+|CSV |[CSVfile.csv](https://storage.googleapis.com/mobile_test_assets/public/CSVfile.csv) |
+|DAT |[Data1KB.dat](https://storage.googleapis.com/mobile_test_assets/public/Data1KB.dat) |
+|DOCX|[MyDocument.docx](https://storage.googleapis.com/mobile_test_assets/public/MyDocument.docx) |
+|DOCX|[MyOldWordDocument.doc](https://storage.googleapis.com/mobile_test_assets/public/MyOldWordDocument.doc) |
+|EXE |[executable.exe](https://storage.googleapis.com/mobile_test_assets/public/executable.exe) |
+|HTML|[htmlFile.html](https://storage.googleapis.com/mobile_test_assets/public/htmlFile.html) |
+|MP3 |[audioSample.mp3](https://storage.googleapis.com/mobile_test_assets/public/audioSample.mp3) |
+|PDF |[lorem\_upsum.pdf](https://storage.googleapis.com/mobile_test_assets/public/lorem_ipsum.pdf) |
+|PDF |[washington.pdf](https://storage.googleapis.com/mobile_test_assets/public/washington.pdf) |
+|PNG |[web\_icon.png](https://storage.googleapis.com/mobile_test_assets/public/web_icon.png) |
+|SVG |[tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg](https://storage.googleapis.com/mobile_test_assets/public/tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg)|
+|TXT |[textfile.txt](https://storage.googleapis.com/mobile_test_assets/public/textfile.txt) |
+|WEBM|[videoSample.webm](https://storage.googleapis.com/mobile_test_assets/public/videoSample.webm) |
+|XML |[XMLfile.xml](https://storage.googleapis.com/mobile_test_assets/public/XMLfile.xml) |
+|ZIP |[100MB.zip](https://storage.googleapis.com/mobile_test_assets/public/100MB.zip) |
+|ZIP |[1GB.zip](https://storage.googleapis.com/mobile_test_assets/public/1GB.zip) |
+|ZIP |[200MB.zip](https://storage.googleapis.com/mobile_test_assets/public/200MB.zip) |
+|ZIP |[smallZip.zip](https://storage.googleapis.com/mobile_test_assets/public/smallZip.zip) |
+
+New assets must be approved and uploaded by the [Mozilla Mobile Test Engineering](https://mana.mozilla.org/wiki/display/MTE/Mobile+Test+Engineering) team.
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/docs/channels.md b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/docs/channels.md
new file mode 100644
index 0000000000..178e76df3d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/docs/channels.md
@@ -0,0 +1,19 @@
+# Espresso/UI Automator Tests on All Channels
+
+When writing Espresso/UI Automator tests, by default, the tests are expected to run on all channels unless otherwise targeted. The provided code snippet below demonstrates a conditional check before running the tests on specific channels.
+
+```
+runWithCondition(
+ // Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
+ // Once this feature lands in RC we should remove the wrapper.
+ activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
+ activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
+ )
+```
+The code uses the `runWithCondition()` function to determine the appropriate channel for the test. It checks if the current version's release channel is either Nightly or Beta using the `activityIntentTestRule.activity.components.core.engine.version.releaseChannel` property.
+
+If the release channel is Nightly or Beta, the test is executed within the `runWithCondition()` block. However, once the feature under test lands in the Release Candidate (RC) channel, we suggest removing the wrapper and allowing the tests to run without any channel-specific condition.
+
+This approach ensures that the tests are executed on all channels during the development and testing phase. However, when the feature stabilizes and reaches the RC channel, the conditional check can be removed to ensure the tests run consistently across all channels.
+
+Please note that the actual implementation of the tests and their behavior may vary depending on the specific testing framework, project structure, and requirements. The provided code snippet serves as an example to showcase the concept of targeting specific channels during test execution.
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AccountSettingsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AccountSettingsRobot.kt
new file mode 100644
index 0000000000..e411fc908e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AccountSettingsRobot.kt
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import org.hamcrest.CoreMatchers
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the URL toolbar.
+ */
+class AccountSettingsRobot {
+ fun verifyBookmarksCheckbox() {
+ Log.i(TAG, "verifyBookmarksCheckbox: Trying to verify that the bookmarks check box is visible")
+ bookmarksCheckbox().check(
+ matches(
+ withEffectiveVisibility(
+ Visibility.VISIBLE,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyBookmarksCheckbox: Verified that the bookmarks check box is visible")
+ }
+
+ fun verifyHistoryCheckbox() {
+ Log.i(TAG, "verifyHistoryCheckbox: Trying to verify that the history check box is visible")
+ historyCheckbox().check(
+ matches(
+ withEffectiveVisibility(
+ Visibility.VISIBLE,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHistoryCheckbox: Verified that the history check box is visible")
+ }
+
+ fun verifySignOutButton() {
+ Log.i(TAG, "verifySignOutButton: Trying to verify that the \"Sign out\" button is visible")
+ signOutButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySignOutButton: Verified that the \"Sign out\" button is visible")
+ }
+
+ fun verifyDeviceName() {
+ Log.i(TAG, "verifyDeviceName: Trying to verify that the \"Device name\" option is visible")
+ deviceName().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeviceName: Verified that the \"Device name\" option is visible")
+ }
+
+ class Transition {
+
+ fun disconnectAccount(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "disconnectAccount: Trying to click the \"Sign out\" button")
+ signOutButton().click()
+ Log.i(TAG, "disconnectAccount: Clicked the \"Sign out\" button")
+ Log.i(TAG, "disconnectAccount: Trying to click the \"Disconnect\" button")
+ disconnectButton().click()
+ Log.i(TAG, "disconnectAccount: Clicked the \"Disconnect\" button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+fun accountSettings(interact: AccountSettingsRobot.() -> Unit): AccountSettingsRobot.Transition {
+ AccountSettingsRobot().interact()
+ return AccountSettingsRobot.Transition()
+}
+
+private fun bookmarksCheckbox() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withText("Bookmarks")))
+private fun historyCheckbox() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withText("History")))
+
+private fun signOutButton() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withText("Sign out")))
+private fun deviceName() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withText("Device name")))
+
+private fun disconnectButton() = Espresso.onView(CoreMatchers.allOf(ViewMatchers.withId(R.id.signOutDisconnect)))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt
new file mode 100644
index 0000000000..f42941bbca
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.os.Build
+import android.util.Log
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import java.util.regex.Pattern
+
+/**
+ * Implementation of Robot Pattern for the Add to homescreen feature.
+ */
+class AddToHomeScreenRobot {
+
+ fun verifyAddPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) {
+ Log.i(TAG, "verifyAddPrivateBrowsingShortcutButton: Trying to verify \"Add to Home screen\" private browsing shortcut dialog button is displayed")
+ composeTestRule.onNodeWithTag("private.add").assertIsDisplayed()
+ Log.i(TAG, "verifyAddPrivateBrowsingShortcutButton: Verified \"Add to Home screen\" private browsing shortcut dialog button is displayed")
+ }
+
+ fun verifyNoThanksPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) {
+ Log.i(TAG, "verifyNoThanksPrivateBrowsingShortcutButton: Trying to verify \"No thanks\" private browsing shortcut dialog button is displayed")
+ composeTestRule.onNodeWithTag("private.cancel").assertIsDisplayed()
+ Log.i(TAG, "verifyNoThanksPrivateBrowsingShortcutButton: Verified \"No thanks\" private browsing shortcut dialog button is displayed")
+ }
+
+ fun clickAddPrivateBrowsingShortcutButton(composeTestRule: ComposeTestRule) {
+ Log.i(TAG, "clickAddPrivateBrowsingShortcutButton: Trying to click \"Add to Home screen\" private browsing shortcut dialog button")
+ composeTestRule.onNodeWithTag("private.add").performClick()
+ Log.i(TAG, "clickAddPrivateBrowsingShortcutButton: Clicked \"Add to Home screen\" private browsing shortcut dialog button")
+ }
+
+ fun addShortcutName(title: String) {
+ Log.i(TAG, "addShortcutName: Trying to set shortcut name to: $title")
+ shortcutTextField().setText(title)
+ Log.i(TAG, "addShortcutName: Set shortcut name to: $title")
+ }
+
+ fun verifyShortcutTextFieldTitle(title: String) = assertUIObjectExists(shortcutTitle(title))
+
+ fun clickAddShortcutButton() {
+ Log.i(TAG, "clickAddShortcutButton: Trying to click \"Add\" button from \"Add to home screen\" dialog and wait for $waitingTime ms for a new window")
+ confirmAddToHomeScreenButton().clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickAddShortcutButton: Clicked \"Add\" button from \"Add to home screen\" dialog and waited for $waitingTime ms for a new window")
+ }
+
+ fun clickCancelShortcutButton() {
+ Log.i(TAG, "clickCancelShortcutButton: Trying to click \"Cancel\" button from \"Add to home screen\" dialog")
+ cancelAddToHomeScreenButton().click()
+ Log.i(TAG, "clickCancelShortcutButton: Clicked \"Cancel\" button from \"Add to home screen\" dialog")
+ }
+
+ fun clickAddAutomaticallyButton() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Log.i(TAG, "clickAddAutomaticallyButton: Waiting for $waitingTime ms until finding \"Add automatically\" system dialog button")
+ mDevice.wait(
+ Until.findObject(
+ By.text(
+ Pattern.compile("Add Automatically", Pattern.CASE_INSENSITIVE),
+ ),
+ ),
+ waitingTime,
+ )
+ Log.i(TAG, "clickAddAutomaticallyButton: Waited for $waitingTime ms until \"Add automatically\" system dialog button was found")
+ Log.i(TAG, "clickAddAutomaticallyButton: Trying to click \"Add automatically\" system dialog button")
+ addAutomaticallyButton().click()
+ Log.i(TAG, "clickAddAutomaticallyButton: Clicked \"Add automatically\" system dialog button")
+ }
+ }
+
+ fun verifyShortcutAdded(shortcutTitle: String) =
+ assertUIObjectExists(itemContainingText(shortcutTitle))
+
+ class Transition {
+ fun openHomeScreenShortcut(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openHomeScreenShortcut: Waiting for $waitingTime ms until finding $title home screen shortcut")
+ mDevice.wait(
+ Until.findObject(By.text(title)),
+ waitingTime,
+ )
+ Log.i(TAG, "openHomeScreenShortcut: Waited for $waitingTime ms until $title home screen shortcut was found")
+ Log.i(TAG, "openHomeScreenShortcut: Trying to click $title home screen shortcut and wait for $waitingTime ms for a new window")
+ mDevice.findObject((UiSelector().text(title))).clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "openHomeScreenShortcut: Clicked $title home screen shortcut and waited for $waitingTime ms for a new window")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun searchAndOpenHomeScreenShortcut(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "searchAndOpenHomeScreenShortcut: Trying to press device home button")
+ mDevice.pressHome()
+ Log.i(TAG, "searchAndOpenHomeScreenShortcut: Pressed device home button")
+
+ fun homeScreenView() = UiScrollable(UiSelector().scrollable(true))
+ Log.i(TAG, "searchAndOpenHomeScreenShortcut: Waiting for $waitingTime ms for home screen view to exist")
+ homeScreenView().waitForExists(waitingTime)
+ Log.i(TAG, "searchAndOpenHomeScreenShortcut: Waited for $waitingTime ms for home screen view to exist")
+
+ fun shortcut() =
+ homeScreenView()
+ .setAsHorizontalList()
+ .getChildByText(UiSelector().textContains(title), title, true)
+ Log.i(TAG, "searchAndOpenHomeScreenShortcut: Trying to click home screen shortcut: $title and wait for a new window")
+ shortcut().clickAndWaitForNewWindow()
+ Log.i(TAG, "searchAndOpenHomeScreenShortcut: Clicked home screen shortcut: $title and waited for a new window")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+fun addToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
+ AddToHomeScreenRobot().interact()
+ return AddToHomeScreenRobot.Transition()
+}
+
+private fun addAutomaticallyButton() =
+ mDevice.findObject(UiSelector().textContains("add automatically"))
+
+private fun cancelAddToHomeScreenButton() =
+ itemWithResId("$packageName:id/cancel_button")
+private fun confirmAddToHomeScreenButton() =
+ itemWithResId("$packageName:id/add_button")
+private fun shortcutTextField() =
+ itemWithResId("$packageName:id/shortcut_text")
+private fun shortcutTitle(title: String) =
+ itemWithResIdAndText("$packageName:id/shortcut_text", title)
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt
new file mode 100644
index 0000000000..007dfddff3
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt
@@ -0,0 +1,517 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.net.Uri
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.clearText
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.action.ViewActions.replaceText
+import androidx.test.espresso.action.ViewActions.typeText
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withChild
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.By.res
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.containsString
+import org.junit.Assert.assertEquals
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+/**
+ * Implementation of Robot Pattern for the bookmarks menu.
+ */
+class BookmarksRobot {
+
+ fun verifyBookmarksMenuView() {
+ Log.i(TAG, "verifyBookmarksMenuView: Waiting for $waitingTime ms for bookmarks view to exist")
+ mDevice.findObject(
+ UiSelector().text("Bookmarks"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifyBookmarksMenuView: Waited for $waitingTime ms for bookmarks view to exist")
+ Log.i(TAG, "verifyBookmarksMenuView: Trying to verify bookmarks view is displayed")
+ onView(
+ allOf(
+ withText("Bookmarks"),
+ withParent(withId(R.id.navigationToolbar)),
+ ),
+ ).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyBookmarksMenuView: Verified bookmarks view is displayed")
+ }
+
+ fun verifyAddFolderButton() {
+ Log.i(TAG, "verifyAddFolderButton: Trying to verify add bookmarks folder button is visible")
+ addFolderButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAddFolderButton: Verified add bookmarks folder button is visible")
+ }
+
+ fun verifyCloseButton() {
+ Log.i(TAG, "verifyCloseButton: Trying to verify close bookmarks section button is visible")
+ closeButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCloseButton: Verified close bookmarks section button is visible")
+ }
+
+ fun verifyBookmarkFavicon(forUrl: Uri) {
+ Log.i(TAG, "verifyBookmarkFavicon: Trying to verify bookmarks favicon for $forUrl is visible")
+ bookmarkFavicon(forUrl.toString()).check(
+ matches(
+ withEffectiveVisibility(
+ ViewMatchers.Visibility.VISIBLE,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyBookmarkFavicon: Verified bookmarks favicon for $forUrl is visible")
+ }
+
+ fun verifyBookmarkedURL(url: String) {
+ Log.i(TAG, "verifyBookmarkedURL: Trying to verify bookmarks url: $url is displayed")
+ bookmarkURL(url).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyBookmarkedURL: Verified bookmarks url: $url is displayed")
+ }
+
+ fun verifyFolderTitle(title: String) {
+ Log.i(TAG, "verifyFolderTitle: Waiting for $waitingTime ms for bookmarks folder with title: $title to exist")
+ mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
+ Log.i(TAG, "verifyFolderTitle: Waited for $waitingTime ms for bookmarks folder with title: $title to exist")
+ Log.i(TAG, "verifyFolderTitle: Trying to verify bookmarks folder with title: $title is displayed")
+ onView(withText(title)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyFolderTitle: Verified bookmarks folder with title: $title is displayed")
+ }
+
+ fun verifyBookmarkFolderIsNotCreated(title: String) {
+ Log.i(TAG, "verifyBookmarkFolderIsNotCreated: Waiting for $waitingTime ms for bookmarks folder with title: $title to exist")
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/bookmarks_wrapper"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifyBookmarkFolderIsNotCreated: Waited for $waitingTime ms for bookmarks folder with title: $title to exist")
+
+ assertUIObjectExists(itemContainingText(title), exists = false)
+ }
+
+ fun verifyBookmarkTitle(title: String) {
+ Log.i(TAG, "verifyBookmarkTitle: Waiting for $waitingTime ms for bookmark with title: $title to exist")
+ mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
+ Log.i(TAG, "verifyBookmarkTitle: Waited for $waitingTime ms for bookmark with title: $title to exist")
+ Log.i(TAG, "verifyBookmarkTitle: Trying to verify bookmark with title: $title is displayed")
+ onView(withText(title)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyBookmarkTitle: Verified bookmark with title: $title is displayed")
+ }
+
+ fun verifyBookmarkIsDeleted(expectedTitle: String) {
+ Log.i(TAG, "verifyBookmarkIsDeleted: Waiting for $waitingTime ms for bookmarks view to exist")
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/bookmarks_wrapper"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifyBookmarkIsDeleted: Waited for $waitingTime ms for bookmarks view to exist")
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/title",
+ expectedTitle,
+ ),
+ exists = false,
+ )
+ }
+
+ fun verifyUndoDeleteSnackBarButton() {
+ Log.i(TAG, "verifyUndoDeleteSnackBarButton: Trying to verify bookmark deletion undo snack bar button")
+ snackBarUndoButton().check(matches(withText("UNDO")))
+ Log.i(TAG, "verifyUndoDeleteSnackBarButton: Verified bookmark deletion undo snack bar button")
+ }
+
+ fun verifySnackBarHidden() {
+ Log.i(TAG, "verifySnackBarHidden: Waiting until undo snack bar button is gone")
+ mDevice.waitNotNull(
+ Until.gone(By.text("UNDO")),
+ waitingTime,
+ )
+ Log.i(TAG, "verifySnackBarHidden: Waited until undo snack bar button was gone")
+ Log.i(TAG, "verifySnackBarHidden: Trying to verify bookmark snack bar does not exist")
+ onView(withId(R.id.snackbar_layout)).check(doesNotExist())
+ Log.i(TAG, "verifySnackBarHidden: Verified bookmark snack bar does not exist")
+ }
+
+ fun verifyEditBookmarksView() =
+ assertUIObjectExists(
+ itemWithDescription("Navigate up"),
+ itemWithText(getStringResource(R.string.edit_bookmark_fragment_title)),
+ itemWithResId("$packageName:id/delete_bookmark_button"),
+ itemWithResId("$packageName:id/save_bookmark_button"),
+ itemWithResId("$packageName:id/bookmarkNameEdit"),
+ itemWithResId("$packageName:id/bookmarkUrlEdit"),
+ itemWithResId("$packageName:id/bookmarkParentFolderSelector"),
+ )
+
+ fun verifyKeyboardHidden(isExpectedToBeVisible: Boolean) {
+ Log.i(TAG, "assertKeyboardVisibility: Trying to verify that the keyboard is visible: $isExpectedToBeVisible")
+ assertEquals(
+ isExpectedToBeVisible,
+ mDevice
+ .executeShellCommand("dumpsys input_method | grep mInputShown")
+ .contains("mInputShown=true"),
+ )
+ Log.i(TAG, "assertKeyboardVisibility: Verified that the keyboard is visible: $isExpectedToBeVisible")
+ }
+
+ fun verifyShareOverlay() {
+ Log.i(TAG, "verifyShareOverlay: Trying to verify bookmarks sharing overlay is displayed")
+ onView(withId(R.id.shareWrapper)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareOverlay: Verified bookmarks sharing overlay is displayed")
+ }
+
+ fun verifyShareBookmarkFavicon() {
+ Log.i(TAG, "verifyShareBookmarkFavicon: Trying to verify shared bookmarks favicon is displayed")
+ onView(withId(R.id.share_tab_favicon)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareBookmarkFavicon: Verified shared bookmarks favicon is displayed")
+ }
+
+ fun verifyShareBookmarkTitle() {
+ Log.i(TAG, "verifyShareBookmarkTitle: Trying to verify shared bookmarks title is displayed")
+ onView(withId(R.id.share_tab_title)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareBookmarkTitle: Verified shared bookmarks title is displayed")
+ }
+
+ fun verifyShareBookmarkUrl() {
+ Log.i(TAG, "verifyShareBookmarkUrl: Trying to verify shared bookmarks url is displayed")
+ onView(withId(R.id.share_tab_url)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareBookmarkUrl: Verified shared bookmarks url is displayed")
+ }
+
+ fun verifyCurrentFolderTitle(title: String) {
+ Log.i(TAG, "verifyCurrentFolderTitle: Waiting for $waitingTime ms for bookmark with title: $title to exist")
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/navigationToolbar")
+ .textContains(title),
+ )
+ .waitForExists(waitingTime)
+ Log.i(TAG, "verifyCurrentFolderTitle: Waited for $waitingTime ms for bookmark with title: $title to exist")
+ Log.i(TAG, "verifyCurrentFolderTitle: Trying to verify bookmark with title: $title is displayed")
+ onView(
+ allOf(
+ withText(title),
+ withParent(withId(R.id.navigationToolbar)),
+ ),
+ )
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCurrentFolderTitle: Verified bookmark with title: $title is displayed")
+ }
+
+ fun waitForBookmarksFolderContentToExist(parentFolderName: String, childFolderName: String) {
+ Log.i(TAG, "waitForBookmarksFolderContentToExist: Waiting for $waitingTime ms for navigation toolbar containing bookmark folder with title: $parentFolderName to exist")
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/navigationToolbar")
+ .textContains(parentFolderName),
+ )
+ .waitForExists(waitingTime)
+ Log.i(TAG, "waitForBookmarksFolderContentToExist: Waited for $waitingTime ms for navigation toolbar containing bookmark folder with title: $parentFolderName to exist")
+
+ mDevice.waitNotNull(Until.findObject(By.text(childFolderName)), waitingTime)
+ }
+
+ fun verifySyncSignInButton() {
+ Log.i(TAG, "verifySyncSignInButton: Trying to verify sign in to sync button is visible")
+ syncSignInButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifySyncSignInButton: Verified sign in to sync button is visible")
+ }
+
+ fun cancelFolderDeletion() {
+ Log.i(TAG, "cancelFolderDeletion: Trying to click \"Cancel\" bookmarks folder deletion dialog button")
+ onView(withText("CANCEL"))
+ .inRoot(RootMatchers.isDialog())
+ .check(matches(isDisplayed()))
+ .click()
+ Log.i(TAG, "cancelFolderDeletion: Clicked \"Cancel\" bookmarks folder deletion dialog button")
+ }
+
+ fun createFolder(name: String, parent: String? = null) {
+ clickAddFolderButton()
+ addNewFolderName(name)
+ if (!parent.isNullOrBlank()) {
+ setParentFolder(parent)
+ }
+ saveNewFolder()
+ }
+
+ fun setParentFolder(parentName: String) {
+ clickParentFolderSelector()
+ selectFolder(parentName)
+ navigateUp()
+ }
+
+ fun clickAddFolderButton() {
+ mDevice.waitNotNull(
+ Until.findObject(By.desc("Add folder")),
+ waitingTime,
+ )
+ Log.i(TAG, "clickAddFolderButton: Trying to click add bookmarks folder button")
+ addFolderButton().click()
+ Log.i(TAG, "clickAddFolderButton: Clicked add bookmarks folder button")
+ }
+
+ fun clickAddNewFolderButtonFromSelectFolderView() {
+ itemWithResId("$packageName:id/add_folder_button")
+ .also {
+ Log.i(TAG, "clickAddNewFolderButtonFromSelectFolderView: Waiting for $waitingTime ms for add bookmarks folder button from folder selection view to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickAddNewFolderButtonFromSelectFolderView: Waited for $waitingTime ms for add bookmarks folder button from folder selection view to exist")
+ Log.i(TAG, "clickAddNewFolderButtonFromSelectFolderView: Trying to click add bookmarks folder button from folder selection view")
+ it.click()
+ Log.i(TAG, "clickAddNewFolderButtonFromSelectFolderView: Clicked add bookmarks folder button from folder selection view")
+ }
+ }
+
+ fun addNewFolderName(name: String) {
+ Log.i(TAG, "addNewFolderName: Trying to click add folder name field")
+ addFolderTitleField().click()
+ Log.i(TAG, "addNewFolderName: Clicked to click add folder name field")
+ Log.i(TAG, "addNewFolderName: Trying to set bookmarks folder name to: $name")
+ addFolderTitleField().perform(replaceText(name))
+ Log.i(TAG, "addNewFolderName: Bookmarks folder name was set to: $name")
+ }
+
+ fun saveNewFolder() {
+ Log.i(TAG, "saveNewFolder: Trying to click save folder button")
+ saveFolderButton().click()
+ Log.i(TAG, "saveNewFolder: Clicked save folder button")
+ }
+
+ fun navigateUp() {
+ Log.i(TAG, "navigateUp: Trying to click navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "navigateUp: Clicked navigate up toolbar button")
+ }
+
+ fun changeBookmarkTitle(newTitle: String) {
+ Log.i(TAG, "changeBookmarkTitle: Trying to clear bookmark name text box")
+ bookmarkNameEditBox().perform(clearText())
+ Log.i(TAG, "changeBookmarkTitle: Cleared bookmark name text box")
+ Log.i(TAG, "changeBookmarkTitle: Trying to set bookmark title to: $newTitle")
+ bookmarkNameEditBox().perform(typeText(newTitle))
+ Log.i(TAG, "changeBookmarkTitle: Bookmark title was set to: $newTitle")
+ }
+
+ fun changeBookmarkUrl(newUrl: String) {
+ Log.i(TAG, "changeBookmarkUrl: Trying to clear bookmark url text box")
+ bookmarkURLEditBox().perform(clearText())
+ Log.i(TAG, "changeBookmarkUrl: Cleared bookmark url text box")
+ Log.i(TAG, "changeBookmarkUrl: Trying to set bookmark url to: $newUrl")
+ bookmarkURLEditBox().perform(typeText(newUrl))
+ Log.i(TAG, "changeBookmarkUrl: Bookmark url was set to: $newUrl")
+ }
+
+ fun saveEditBookmark() {
+ Log.i(TAG, "saveEditBookmark: Trying to click save bookmark button")
+ saveBookmarkButton().click()
+ Log.i(TAG, "saveEditBookmark: Clicked save bookmark button")
+ Log.i(TAG, "saveEditBookmark: Waiting for $waitingTime ms for bookmarks list to exist")
+ mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/bookmark_list")).waitForExists(waitingTime)
+ Log.i(TAG, "saveEditBookmark: Waited for $waitingTime ms for bookmarks list to exist")
+ }
+
+ fun clickParentFolderSelector() {
+ Log.i(TAG, "clickParentFolderSelector: Trying to click folder selector")
+ bookmarkFolderSelector().click()
+ Log.i(TAG, "clickParentFolderSelector: Clicked folder selector")
+ }
+
+ fun selectFolder(title: String) {
+ Log.i(TAG, "selectFolder: Trying to click folder with title: $title")
+ onView(withText(title)).click()
+ Log.i(TAG, "selectFolder: Clicked folder with title: $title")
+ }
+
+ fun longTapDesktopFolder(title: String) {
+ Log.i(TAG, "longTapDesktopFolder: Trying to long tap folder with title: $title")
+ onView(withText(title)).perform(longClick())
+ Log.i(TAG, "longTapDesktopFolder: Long tapped folder with title: $title")
+ }
+
+ fun cancelDeletion() {
+ val cancelButton = mDevice.findObject(UiSelector().textContains("CANCEL"))
+ Log.i(TAG, "cancelDeletion: Waiting for $waitingTime ms for \"Cancel\" bookmarks deletion button to exist")
+ cancelButton.waitForExists(waitingTime)
+ Log.i(TAG, "cancelDeletion: Waited for $waitingTime ms for \"Cancel\" bookmarks deletion button to exist")
+ Log.i(TAG, "cancelDeletion: Trying to click \"Cancel\" bookmarks deletion button")
+ cancelButton.click()
+ Log.i(TAG, "cancelDeletion: Clicked \"Cancel\" bookmarks deletion button")
+ }
+
+ fun confirmDeletion() {
+ Log.i(TAG, "confirmDeletion: Trying to click \"Delete\" bookmarks deletion button")
+ onView(withText(R.string.delete_browsing_data_prompt_allow))
+ .inRoot(RootMatchers.isDialog())
+ .check(matches(isDisplayed()))
+ .click()
+ Log.i(TAG, "confirmDeletion: Clicked \"Delete\" bookmarks deletion button")
+ }
+
+ fun clickDeleteInEditModeButton() {
+ Log.i(TAG, "clickDeleteInEditModeButton: Trying to click delete bookmarks button while in edit mode")
+ deleteInEditModeButton().click()
+ Log.i(TAG, "clickDeleteInEditModeButton: Clicked delete bookmarks button while in edit mode")
+ }
+
+ class Transition {
+ fun closeMenu(interact: HomeScreenRobot.() -> Unit): Transition {
+ Log.i(TAG, "closeMenu: Trying to click close bookmarks section button")
+ closeButton().click()
+ Log.i(TAG, "closeMenu: Clicked close bookmarks section button")
+
+ HomeScreenRobot().interact()
+ return Transition()
+ }
+
+ fun openThreeDotMenu(bookmark: String, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition {
+ mDevice.waitNotNull(Until.findObject(res("$packageName:id/overflow_menu")))
+ Log.i(TAG, "openThreeDotMenu: Trying to click three dot button for bookmark item: $bookmark")
+ threeDotMenu(bookmark).click()
+ Log.i(TAG, "openThreeDotMenu: Clicked three dot button for bookmark item: $bookmark")
+
+ ThreeDotMenuBookmarksRobot().interact()
+ return ThreeDotMenuBookmarksRobot.Transition()
+ }
+
+ fun clickSingInToSyncButton(interact: SettingsTurnOnSyncRobot.() -> Unit): SettingsTurnOnSyncRobot.Transition {
+ Log.i(TAG, "clickSingInToSyncButton: Trying to click sign in to sync button")
+ syncSignInButton().click()
+ Log.i(TAG, "clickSingInToSyncButton: Clicked sign in to sync button")
+
+ SettingsTurnOnSyncRobot().interact()
+ return SettingsTurnOnSyncRobot.Transition()
+ }
+
+ fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click go back button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked go back button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goBackToBrowserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goBackToBrowserScreen: Trying to click go back button")
+ goBackButton().click()
+ Log.i(TAG, "goBackToBrowserScreen: Clicked go back button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun closeEditBookmarkSection(interact: BookmarksRobot.() -> Unit): Transition {
+ Log.i(TAG, "goBackToBrowserScreen: Trying to click go back button")
+ goBackButton().click()
+ Log.i(TAG, "goBackToBrowserScreen: Clicked go back button")
+
+ BookmarksRobot().interact()
+ return Transition()
+ }
+
+ fun openBookmarkWithTitle(bookmarkTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ itemWithResIdAndText("$packageName:id/title", bookmarkTitle)
+ .also {
+ Log.i(TAG, "openBookmarkWithTitle: Waiting for $waitingTime ms for bookmark with title: $bookmarkTitle")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "openBookmarkWithTitle: Waited for $waitingTime ms for bookmark with title: $bookmarkTitle")
+ Log.i(TAG, "openBookmarkWithTitle: Trying to click bookmark with title: $bookmarkTitle and wait for $waitingTimeShort ms for a new window")
+ it.clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "openBookmarkWithTitle: Clicked bookmark with title: $bookmarkTitle and waited for $waitingTimeShort ms for a new window")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickSearchButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "clickSearchButton: Trying to click search bookmarks button")
+ itemWithResId("$packageName:id/bookmark_search").click()
+ Log.i(TAG, "clickSearchButton: Clicked search bookmarks button")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+ }
+}
+
+fun bookmarksMenu(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+}
+
+private fun closeButton() = onView(withId(R.id.close_bookmarks))
+
+private fun goBackButton() = onView(withContentDescription("Navigate up"))
+
+private fun bookmarkFavicon(url: String) = onView(
+ allOf(
+ withId(R.id.favicon),
+ withParent(
+ withParent(
+ withChild(allOf(withId(R.id.url), withText(url))),
+ ),
+ ),
+ ),
+)
+
+private fun bookmarkURL(url: String) = onView(allOf(withId(R.id.url), withText(containsString(url))))
+
+private fun addFolderButton() = onView(withId(R.id.add_bookmark_folder))
+
+private fun addFolderTitleField() = onView(withId(R.id.bookmarkNameEdit))
+
+private fun saveFolderButton() = onView(withId(R.id.confirm_add_folder_button))
+
+private fun threeDotMenu(bookmark: String) = onView(
+ allOf(
+ withId(R.id.overflow_menu),
+ hasSibling(withText(bookmark)),
+ ),
+)
+
+private fun snackBarText() = onView(withId(R.id.snackbar_text))
+
+private fun snackBarUndoButton() = onView(withId(R.id.snackbar_btn))
+
+private fun bookmarkNameEditBox() = onView(withId(R.id.bookmarkNameEdit))
+
+private fun bookmarkFolderSelector() = onView(withId(R.id.bookmarkParentFolderSelector))
+
+private fun bookmarkURLEditBox() = onView(withId(R.id.bookmarkUrlEdit))
+
+private fun saveBookmarkButton() = onView(withId(R.id.save_bookmark_button))
+
+private fun deleteInEditModeButton() = onView(withId(R.id.delete_bookmark_button))
+
+private fun syncSignInButton() = onView(withId(R.id.bookmark_folders_sign_in))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt
new file mode 100644
index 0000000000..f758c55134
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt
@@ -0,0 +1,1477 @@
+/* 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("TooManyFunctions", "TooGenericExceptionCaught")
+
+package org.mozilla.fenix.ui.robots
+
+import android.content.Context
+import android.net.Uri
+import android.os.SystemClock
+import android.util.Log
+import android.widget.TimePicker
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.PickerActions
+import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.By.text
+import androidx.test.uiautomator.UiObject
+import androidx.test.uiautomator.UiObjectNotFoundException
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.mediasession.MediaSession
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.waitForObjects
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.tabstray.TabsTrayTestTag
+import org.mozilla.fenix.utils.Settings
+import java.time.LocalDate
+
+class BrowserRobot {
+ private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
+
+ fun waitForPageToLoad() = assertUIObjectIsGone(progressBar())
+
+ fun verifyCurrentPrivateSession(context: Context) {
+ val selectedTab = context.components.core.store.state.selectedTab
+ Log.i(TAG, "verifyCurrentPrivateSession: Trying to verify that current browsing session is private")
+ assertTrue("Current session is private", selectedTab?.content?.private ?: false)
+ Log.i(TAG, "verifyCurrentPrivateSession: Verified that current browsing session is private")
+ }
+
+ fun verifyUrl(url: String) {
+ sessionLoadedIdlingResource = SessionLoadedIdlingResource()
+
+ registerAndCleanupIdlingResources(sessionLoadedIdlingResource) {
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/mozac_browser_toolbar_url_view",
+ url.replace("http://", ""),
+ ),
+ )
+ }
+ }
+
+ fun verifyHelpUrl() {
+ verifyUrl("support.mozilla.org/")
+ }
+
+ fun verifyWhatsNewURL() {
+ verifyUrl("mozilla.org/")
+ }
+
+ fun verifyRateOnGooglePlayURL() {
+ verifyUrl("play.google.com/store/apps/details?id=org.mozilla.fenix")
+ }
+
+ /* Asserts that the text within DOM element with ID="testContent" has the given text, i.e.
+ * document.querySelector('#testContent').innerText == expectedText
+ *
+ */
+ fun verifyPageContent(expectedText: String) {
+ sessionLoadedIdlingResource = SessionLoadedIdlingResource()
+
+ mDevice.waitNotNull(
+ Until.findObject(By.res("$packageName:id/engineView")),
+ waitingTime,
+ )
+
+ registerAndCleanupIdlingResources(sessionLoadedIdlingResource) {
+ assertUIObjectExists(itemContainingText(expectedText))
+ }
+ }
+
+ /* Verifies the information displayed on the about:cache page */
+ fun verifyNetworkCacheIsEmpty(storage: String) {
+ val memorySection = mDevice.findObject(UiSelector().description(storage))
+
+ val gridView =
+ if (storage == "memory") {
+ memorySection.getFromParent(
+ UiSelector()
+ .className("android.widget.GridView")
+ .index(2),
+ )
+ } else {
+ memorySection.getFromParent(
+ UiSelector()
+ .className("android.widget.GridView")
+ .index(4),
+ )
+ }
+
+ val cacheSizeInfo =
+ gridView.getChild(
+ UiSelector().text("Number of entries:"),
+ ).getFromParent(
+ UiSelector().text("0"),
+ )
+
+ for (i in 1..RETRY_COUNT) {
+ try {
+ assertUIObjectExists(cacheSizeInfo)
+ break
+ } catch (e: AssertionError) {
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage { }
+ }
+ }
+ }
+
+ fun verifyTabCounter(expectedText: String) =
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/counter_text",
+ expectedText,
+ ),
+ )
+
+ fun verifyContextMenuForLocalHostLinks(containsURL: Uri) {
+ // If the link is directing to another local asset the "Download link" option is not available
+ // If the link is not re-directing to an external app the "Open link in external app" option is not available
+ assertUIObjectExists(
+ contextMenuLinkUrl(containsURL.toString()),
+ contextMenuOpenLinkInNewTab(),
+ contextMenuOpenLinkInPrivateTab(),
+ contextMenuCopyLink(),
+ contextMenuShareLink(),
+ )
+ }
+
+ fun verifyContextMenuForLinksToOtherApps(containsURL: String) {
+ // If the link is re-directing to an external app the "Open link in external app" option is available
+ // If the link is not directing to another local asset the "Download link" option is not available
+ assertUIObjectExists(
+ contextMenuLinkUrl(containsURL),
+ contextMenuOpenLinkInNewTab(),
+ contextMenuOpenLinkInPrivateTab(),
+ contextMenuCopyLink(),
+ contextMenuDownloadLink(),
+ contextMenuShareLink(),
+ contextMenuOpenInExternalApp(),
+ )
+ }
+
+ fun verifyContextMenuForLinksToOtherHosts(containsURL: Uri) {
+ // If the link is re-directing to another host the "Download link" option is available
+ // If the link is not re-directing to an external app the "Open link in external app" option is not available
+ assertUIObjectExists(
+ contextMenuLinkUrl(containsURL.toString()),
+ contextMenuOpenLinkInNewTab(),
+ contextMenuOpenLinkInPrivateTab(),
+ contextMenuCopyLink(),
+ contextMenuDownloadLink(),
+ contextMenuShareLink(),
+ )
+ }
+
+ fun verifyLinkImageContextMenuItems(containsURL: Uri) {
+ mDevice.waitNotNull(Until.findObject(By.textContains(containsURL.toString())))
+ mDevice.waitNotNull(
+ Until.findObject(text("Open link in new tab")),
+ waitingTime,
+ )
+ mDevice.waitNotNull(
+ Until.findObject(text("Open link in private tab")),
+ waitingTime,
+ )
+ mDevice.waitNotNull(Until.findObject(text("Copy link")), waitingTime)
+ mDevice.waitNotNull(Until.findObject(text("Share link")), waitingTime)
+ mDevice.waitNotNull(
+ Until.findObject(text("Open image in new tab")),
+ waitingTime,
+ )
+ mDevice.waitNotNull(Until.findObject(text("Save image")), waitingTime)
+ mDevice.waitNotNull(
+ Until.findObject(text("Copy image location")),
+ waitingTime,
+ )
+ }
+
+ fun verifyNavURLBarHidden() = assertUIObjectIsGone(navURLBar())
+
+ fun verifyMenuButton() {
+ Log.i(TAG, "verifyMenuButton: Trying to verify main menu button is displayed")
+ threeDotButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyMenuButton: Verified main menu button is displayed")
+ }
+
+ fun verifyNoLinkImageContextMenuItems(containsURL: Uri) {
+ mDevice.waitNotNull(Until.findObject(By.textContains(containsURL.toString())))
+ mDevice.waitNotNull(
+ Until.findObject(text("Open image in new tab")),
+ waitingTime,
+ )
+ mDevice.waitNotNull(Until.findObject(text("Save image")), waitingTime)
+ mDevice.waitNotNull(
+ Until.findObject(text("Copy image location")),
+ waitingTime,
+ )
+ }
+
+ fun verifyNotificationDotOnMainMenu() =
+ assertUIObjectExists(itemWithResId("$packageName:id/notification_dot"))
+
+ fun dismissContentContextMenu() {
+ Log.i(TAG, "dismissContentContextMenu: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "dismissContentContextMenu: Clicked device back button")
+ assertUIObjectExists(itemWithResId("$packageName:id/engineView"))
+ }
+
+ fun createBookmark(url: Uri, folder: String? = null) {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(url) {
+ // needs to wait for the right url to load before saving a bookmark
+ verifyUrl(url.toString())
+ }.openThreeDotMenu {
+ }.bookmarkPage {
+ }.takeIf { !folder.isNullOrBlank() }?.let {
+ it.openThreeDotMenu {
+ }.editBookmarkPage {
+ setParentFolder(folder!!)
+ saveEditBookmark()
+ }
+ }
+ }
+
+ fun longClickPDFImage() = longClickPageObject(itemWithResId("pdfjs_internal_id_13R"))
+
+ fun verifyPDFReaderToolbarItems() =
+ assertUIObjectExists(
+ itemWithResIdContainingText("download", "Download"),
+ )
+
+ fun clickSubmitLoginButton() {
+ clickPageObject(itemWithResId("submit"))
+ assertUIObjectIsGone(itemWithResId("submit"))
+ Log.i(TAG, "clickSubmitLoginButton: Waiting for device to be idle for $waitingTimeLong ms")
+ mDevice.waitForIdle(waitingTimeLong)
+ Log.i(TAG, "clickSubmitLoginButton: Waited for device to be idle for $waitingTimeLong ms")
+ }
+
+ fun enterPassword(password: String) {
+ clickPageObject(itemWithResId("password"))
+ setPageObjectText(itemWithResId("password"), password)
+
+ assertUIObjectIsGone(itemWithText(password))
+ }
+
+ /**
+ * Get the current playback state of the currently selected tab.
+ * The result may be null if there if the currently playing media tab cannot be found in [store]
+ *
+ * @param store [BrowserStore] from which to get data about the current tab's state.
+ * @return nullable [MediaSession.PlaybackState] indicating the media playback state for the current tab.
+ */
+ private fun getCurrentPlaybackState(store: BrowserStore): MediaSession.PlaybackState? {
+ return store.state.selectedTab?.mediaSessionState?.playbackState
+ }
+
+ /**
+ * Asserts that in [waitingTime] the playback state of the current tab will be [expectedState].
+ *
+ * @param store [BrowserStore] from which to get data about the current tab's state.
+ * @param expectedState [MediaSession.PlaybackState] the playback state that will be asserted
+ * @param waitingTime maximum time the test will wait for the playback state to become [expectedState]
+ * before failing the assertion.
+ */
+ fun assertPlaybackState(store: BrowserStore, expectedState: MediaSession.PlaybackState) {
+ val startMills = SystemClock.uptimeMillis()
+ var currentMills: Long = 0
+ while (currentMills <= waitingTime) {
+ if (expectedState == getCurrentPlaybackState(store)) return
+ currentMills = SystemClock.uptimeMillis() - startMills
+ }
+ fail("Playback did not moved to state: $expectedState")
+ }
+
+ fun swipeNavBarRight(tabUrl: String) {
+ // failing to swipe on Firebase sometimes, so it tries again
+ try {
+ Log.i(TAG, "swipeNavBarRight: Try block")
+ Log.i(TAG, "swipeNavBarRight: Trying to perform swipe right action on navigation toolbar")
+ navURLBar().swipeRight(2)
+ Log.i(TAG, "swipeNavBarRight: Performed swipe right action on navigation toolbar")
+ assertUIObjectIsGone(itemWithText(tabUrl))
+ } catch (e: AssertionError) {
+ Log.i(TAG, "swipeNavBarRight: AssertionError caught, executing fallback methods")
+ Log.i(TAG, "swipeNavBarRight: Trying to perform swipe right action on navigation toolbar")
+ navURLBar().swipeRight(2)
+ Log.i(TAG, "swipeNavBarRight: Performed swipe right action on navigation toolbar")
+ assertUIObjectIsGone(itemWithText(tabUrl))
+ }
+ }
+
+ fun swipeNavBarLeft(tabUrl: String) {
+ // failing to swipe on Firebase sometimes, so it tries again
+ try {
+ Log.i(TAG, "swipeNavBarLeft: Try block")
+ Log.i(TAG, "swipeNavBarLeft: Trying to perform swipe left action on navigation toolbar")
+ navURLBar().swipeLeft(2)
+ Log.i(TAG, "swipeNavBarLeft: Performed swipe left action on navigation toolbar")
+ assertUIObjectIsGone(itemWithText(tabUrl))
+ } catch (e: AssertionError) {
+ Log.i(TAG, "swipeNavBarLeft: AssertionError caught, executing fallback methods")
+ Log.i(TAG, "swipeNavBarLeft: Trying to perform swipe left action on navigation toolbar")
+ navURLBar().swipeLeft(2)
+ Log.i(TAG, "swipeNavBarLeft: Performed swipe left action on navigation toolbar")
+ assertUIObjectIsGone(itemWithText(tabUrl))
+ }
+ }
+
+ fun clickSuggestedLoginsButton() {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "clickSuggestedLoginsButton: Started try #$i")
+ mDevice.waitForObjects(suggestedLogins())
+ Log.i(TAG, "clickSuggestedLoginsButton: Trying to click suggested logins button")
+ suggestedLogins().click()
+ Log.i(TAG, "clickSuggestedLoginsButton: Clicked suggested logins button")
+ mDevice.waitForObjects(suggestedLogins())
+ break
+ } catch (e: UiObjectNotFoundException) {
+ Log.i(TAG, "clickSuggestedLoginsButton: UiObjectNotFoundException caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ clickPageObject(itemWithResId("username"))
+ }
+ }
+ }
+ }
+
+ fun setTextForApartmentTextBox(apartment: String) {
+ Log.i(TAG, "setTextForApartmentTextBox: Trying to set the text for the apartment text box to: $apartment")
+ itemWithResId("apartment").setText(apartment)
+ Log.i(TAG, "setTextForApartmentTextBox: The text for the apartment text box was set to: $apartment")
+ }
+
+ fun clearAddressForm() {
+ clearTextFieldItem(itemWithResId("streetAddress"))
+ clearTextFieldItem(itemWithResId("city"))
+ clearTextFieldItem(itemWithResId("country"))
+ clearTextFieldItem(itemWithResId("zipCode"))
+ clearTextFieldItem(itemWithResId("telephone"))
+ clearTextFieldItem(itemWithResId("email"))
+ }
+
+ fun clickSelectAddressButton() {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "clickSelectAddressButton: Started try #$i")
+ assertUIObjectExists(selectAddressButton())
+ Log.i(TAG, "clickSelectAddressButton: Trying to click the select address button and wait for $waitingTime ms for a new window")
+ selectAddressButton().clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickSelectAddressButton: Clicked the select address button and waited for $waitingTime ms for a new window")
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "clickSelectAddressButton: AssertionError caught, executing fallback methods")
+ // Retrying to trigger the prompt, in case we hit https://bugzilla.mozilla.org/show_bug.cgi?id=1816869
+ // This should be removed when the bug is fixed.
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ clickPageObject(itemWithResId("city"))
+ clickPageObject(itemWithResId("country"))
+ }
+ }
+ }
+ }
+
+ fun verifySelectAddressButtonExists(exists: Boolean) = assertUIObjectExists(selectAddressButton(), exists = exists)
+
+ fun changeCreditCardExpiryDate(expiryDate: String) {
+ Log.i(TAG, "changeCreditCardExpiryDate: Trying to set credit card expiry date to: $expiryDate")
+ itemWithResId("expiryMonthAndYear").setText(expiryDate)
+ Log.i(TAG, "changeCreditCardExpiryDate: Credit card expiry date was set to: $expiryDate")
+ }
+
+ fun clickCreditCardNumberTextBox() {
+ Log.i(TAG, "clickCreditCardNumberTextBox: Waiting for $waitingTime ms until finding the credit card number text box")
+ mDevice.wait(Until.findObject(By.res("cardNumber")), waitingTime)
+ Log.i(TAG, "clickCreditCardNumberTextBox: Waited for $waitingTime ms until the credit card number text box was found")
+ Log.i(TAG, "clickCreditCardNumberTextBox: Trying to click the credit card number text box")
+ mDevice.findObject(By.res("cardNumber")).click()
+ Log.i(TAG, "clickCreditCardNumberTextBox: Clicked the credit card number text box")
+ Log.i(TAG, "clickCreditCardNumberTextBox: Waiting for $waitingTimeShort ms for $appName window to be updated")
+ mDevice.waitForWindowUpdate(appName, waitingTimeShort)
+ Log.i(TAG, "clickCreditCardNumberTextBox: Waited for $waitingTimeShort ms for $appName window to be updated")
+ }
+
+ fun clickCreditCardFormSubmitButton() {
+ Log.i(TAG, "clickCreditCardFormSubmitButton: Trying to click the credit card form submit button and wait for $waitingTime ms for a new window")
+ itemWithResId("submit").clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickCreditCardFormSubmitButton: Clicked the credit card form submit button and waited for $waitingTime ms for a new window")
+ }
+
+ fun fillAndSaveCreditCard(cardNumber: String, cardName: String, expiryMonthAndYear: String) {
+ Log.i(TAG, "fillAndSaveCreditCard: Tying to set credit card number to: $cardNumber")
+ itemWithResId("cardNumber").setText(cardNumber)
+ Log.i(TAG, "fillAndSaveCreditCard: Credit card number was set to: $cardNumber")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to set credit card name to: $cardName")
+ itemWithResId("nameOnCard").setText(cardName)
+ Log.i(TAG, "fillAndSaveCreditCard: Credit card name was set to: $cardName")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to set credit card expiry month and year to: $expiryMonthAndYear")
+ itemWithResId("expiryMonthAndYear").setText(expiryMonthAndYear)
+ Log.i(TAG, "fillAndSaveCreditCard: Credit card expiry month and year were set to: $expiryMonthAndYear")
+ Log.i(TAG, "fillAndSaveCreditCard: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "fillAndSaveCreditCard: Waited for device to be idle for $waitingTime ms")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to click the credit card form submit button and wait for $waitingTime ms for a new window")
+ itemWithResId("submit").clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "fillAndSaveCreditCard: Clicked the credit card form submit button and waited for $waitingTime ms for a new window")
+ waitForPageToLoad()
+ Log.i(TAG, "fillAndSaveCreditCard: Waiting for $waitingTime ms for $packageName window to be updated")
+ mDevice.waitForWindowUpdate(packageName, waitingTime)
+ Log.i(TAG, "fillAndSaveCreditCard: Waited for $waitingTime ms for $packageName window to be updated")
+ }
+
+ fun verifyUpdateOrSaveCreditCardPromptExists(exists: Boolean) =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/save_credit_card_header"),
+ exists = exists,
+ )
+
+ fun verifySelectCreditCardPromptExists(exists: Boolean) =
+ assertUIObjectExists(selectCreditCardButton(), exists = exists)
+
+ fun verifyCreditCardSuggestion(vararg creditCardNumbers: String) {
+ for (creditCardNumber in creditCardNumbers) {
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/credit_card_number",
+ creditCardNumber,
+ ),
+ )
+ }
+ }
+
+ fun verifySuggestedUserName(userName: String) {
+ Log.i(TAG, "verifySuggestedUserName: Waiting for $waitingTime ms for suggested logins fragment to exist")
+ itemWithResId("$packageName:id/mozac_feature_login_multiselect_expand").waitForExists(waitingTime)
+ Log.i(TAG, "verifySuggestedUserName: Waited for $waitingTime ms for suggested logins fragment to exist")
+ assertUIObjectExists(itemContainingText(userName))
+ }
+
+ fun verifyPrefilledLoginCredentials(userName: String, password: String, credentialsArePrefilled: Boolean) {
+ // Sometimes the assertion of the pre-filled logins fails so we are re-trying after refreshing the page
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifyPrefilledLoginCredentials: Started try #$i")
+ mDevice.waitForObjects(itemWithResId("username"))
+ assertItemTextEquals(itemWithResId("username"), expectedText = userName, isEqual = credentialsArePrefilled)
+ mDevice.waitForObjects(itemWithResId("password"))
+ assertItemTextEquals(itemWithResId("password"), expectedText = password, isEqual = credentialsArePrefilled)
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyPrefilledLoginCredentials: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ clearTextFieldItem(itemWithResId("username"))
+ clickSuggestedLoginsButton()
+ verifySuggestedUserName(userName)
+ clickPageObject(itemWithResIdAndText("$packageName:id/username", userName))
+ clickPageObject(itemWithResId("togglePassword"))
+ }
+ }
+ }
+ }
+ }
+
+ fun verifyAutofilledAddress(streetAddress: String) {
+ mDevice.waitForObjects(itemWithResIdAndText("streetAddress", streetAddress))
+ assertUIObjectExists(itemWithResIdAndText("streetAddress", streetAddress))
+ }
+
+ fun verifyManuallyFilledAddress(apartment: String) {
+ mDevice.waitForObjects(itemWithResIdAndText("apartment", apartment))
+ assertUIObjectExists(itemWithResIdAndText("apartment", apartment))
+ }
+
+ fun verifyAutofilledCreditCard(creditCardNumber: String) {
+ mDevice.waitForObjects(itemWithResIdAndText("cardNumber", creditCardNumber))
+ assertUIObjectExists(itemWithResIdAndText("cardNumber", creditCardNumber))
+ }
+
+ fun verifySaveLoginPromptIsDisplayed() =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/feature_prompt_login_fragment"),
+ )
+
+ fun verifySaveLoginPromptIsNotDisplayed() =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/feature_prompt_login_fragment"),
+ exists = false,
+ )
+
+ fun verifyTrackingProtectionWebContent(state: String) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifyTrackingProtectionWebContent: Started try #$i")
+ assertUIObjectExists(itemContainingText(state))
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyTrackingProtectionWebContent: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ Log.e(TAG, "On try $i, trackers are not: $state")
+
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }
+ }
+ }
+ }
+ }
+
+ fun verifyCookiesProtectionHintIsDisplayed(composeTestRule: HomeActivityComposeTestRule, isDisplayed: Boolean) {
+ if (isDisplayed) {
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Trying to verify that the total cookie protection message is displayed")
+ composeTestRule.onNodeWithTag("tcp_cfr.message").assertIsDisplayed()
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Verified total cookie protection message is displayed")
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Trying to verify that the total cookie protection learn more link is displayed")
+ composeTestRule.onNodeWithTag("tcp_cfr.action").assertIsDisplayed()
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Verified that the total cookie protection learn more link is displayed")
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Trying to verify that the total cookie protection dismiss button is displayed")
+ composeTestRule.onNodeWithTag("cfr.dismiss").assertIsDisplayed()
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Verified total cookie protection dismiss button is displayed")
+ } else {
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Trying to verify that the total cookie protection message does not exist")
+ composeTestRule.onNodeWithTag("tcp_cfr.message").assertDoesNotExist()
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Verified that the total cookie protection message does not exist")
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Trying to verify that the total cookie protection learn more link does not exist")
+ composeTestRule.onNodeWithTag("tcp_cfr.action").assertDoesNotExist()
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Verified total cookie protection learn more link does not exist")
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Trying to verify that the total cookie protection dismiss button does not exist")
+ composeTestRule.onNodeWithTag("cfr.dismiss").assertDoesNotExist()
+ Log.i(TAG, "verifyCookiesProtectionHintIsDisplayed: Verified that the total cookie protection dismiss button does not exist")
+ }
+ }
+
+ fun clickTCPCFRLearnMore(composeTestRule: HomeActivityComposeTestRule) {
+ Log.i(TAG, "clickTCPCFRLearnMore: Trying to click the total cookie protection learn more link")
+ composeTestRule.onNodeWithTag("tcp_cfr.action").performClick()
+ Log.i(TAG, "clickTCPCFRLearnMore: Clicked total cookie protection learn more link")
+ }
+
+ fun dismissTCPCFRPopup(composeTestRule: HomeActivityComposeTestRule) {
+ Log.i(TAG, "dismissTCPCFRPopup: Trying to click the total cookie protection dismiss button")
+ composeTestRule.onNodeWithTag("cfr.dismiss").performClick()
+ Log.i(TAG, "dismissTCPCFRPopup: Clicked total cookie protection dismiss button")
+ }
+
+ fun verifyShouldShowCFRTCP(shouldShow: Boolean, settings: Settings) {
+ if (shouldShow) {
+ Log.i(TAG, "verifyShouldShowCFRTCP: Trying to verify that the TCP CFR should be shown")
+ assertTrue(settings.shouldShowTotalCookieProtectionCFR)
+ Log.i(TAG, "verifyShouldShowCFRTCP: Verified that the TCP CFR should be shown")
+ } else {
+ Log.i(TAG, "verifyShouldShowCFRTCP: Trying to verify that the TCP CFR should not be shown")
+ assertFalse(settings.shouldShowTotalCookieProtectionCFR)
+ Log.i(TAG, "verifyShouldShowCFRTCP: Verified that the TCP CFR should not be shown")
+ }
+ }
+
+ fun selectTime(hour: Int, minute: Int) {
+ Log.i(TAG, "selectTime: Trying to select time picker hour: $hour and minute: $minute")
+ onView(
+ isAssignableFrom(TimePicker::class.java),
+ ).inRoot(
+ isDialog(),
+ ).perform(PickerActions.setTime(hour, minute))
+ Log.i(TAG, "selectTime: Selected time picker hour: $hour and minute: $minute")
+ }
+
+ fun verifySelectedDate() {
+ val currentDate = LocalDate.now()
+ val currentDay = currentDate.dayOfMonth
+ val currentMonth = currentDate.month
+ val currentYear = currentDate.year
+
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifySelectedDate: Started try #$i")
+ assertUIObjectExists(itemContainingText("Selected date is: $currentDate"))
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifySelectedDate: AssertionError caught, executing fallback methods")
+ Log.e(TAG, "Selected time isn't displayed ${e.localizedMessage}")
+
+ clickPageObject(itemWithResId("calendar"))
+ clickPageObject(itemWithDescription("$currentDay $currentMonth $currentYear"))
+ clickPageObject(itemContainingText("OK"))
+ clickPageObject(itemWithResId("submitDate"))
+ }
+ }
+
+ assertUIObjectExists(itemContainingText("Selected date is: $currentDate"))
+ }
+
+ fun verifyNoDateIsSelected() {
+ val currentDate = LocalDate.now()
+ assertUIObjectExists(
+ itemContainingText("Selected date is: $currentDate"),
+ exists = false,
+ )
+ }
+
+ fun verifySelectedTime(hour: Int, minute: Int) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifySelectedTime: Started try #$i")
+ assertUIObjectExists(itemContainingText("Selected time is: $hour:$minute"))
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifySelectedTime: AssertionError caught, executing fallback methods")
+ Log.e(TAG, "Selected time isn't displayed ${e.localizedMessage}")
+
+ clickPageObject(itemWithResId("clock"))
+ clickPageObject(itemContainingText("CLEAR"))
+ clickPageObject(itemWithResId("clock"))
+ selectTime(hour, minute)
+ clickPageObject(itemContainingText("OK"))
+ clickPageObject(itemWithResId("submitTime"))
+ }
+ }
+ assertUIObjectExists(itemContainingText("Selected time is: $hour:$minute"))
+ }
+
+ fun verifySelectedColor(hexValue: String) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifySelectedColor: Started try #$i")
+ assertUIObjectExists(itemContainingText("Selected color is: $hexValue"))
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifySelectedColor: AssertionError caught, executing fallback methods")
+ Log.e(TAG, "Selected color isn't displayed ${e.localizedMessage}")
+
+ clickPageObject(itemWithResId("colorPicker"))
+ clickPageObject(itemWithDescription(hexValue))
+ clickPageObject(itemContainingText("SET"))
+ clickPageObject(itemWithResId("submitColor"))
+ }
+ }
+
+ assertUIObjectExists(itemContainingText("Selected color is: $hexValue"))
+ }
+
+ fun verifySelectedDropDownOption(optionName: String) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifySelectedDropDownOption: Started try #$i")
+ Log.i(TAG, "verifySelectedDropDownOption: Waiting for $waitingTime ms for \"Submit drop down option\" form button to exist")
+ mDevice.findObject(
+ UiSelector()
+ .textContains("Submit drop down option")
+ .resourceId("submitOption"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifySelectedDropDownOption: Waited for $waitingTime ms for \"Submit drop down option\" form button to exist")
+ assertUIObjectExists(itemContainingText("Selected option is: $optionName"))
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifySelectedDropDownOption: AssertionError caught, executing fallback methods")
+ Log.e(TAG, "Selected option isn't displayed ${e.localizedMessage}")
+
+ clickPageObject(itemWithResId("dropDown"))
+ clickPageObject(itemContainingText(optionName))
+ clickPageObject(itemWithResId("submitOption"))
+ }
+ }
+
+ assertUIObjectExists(itemContainingText("Selected option is: $optionName"))
+ }
+
+ fun verifyNoTimeIsSelected(hour: Int, minute: Int) =
+ assertUIObjectExists(itemContainingText("Selected date is: $hour:$minute"), exists = false)
+
+ fun verifyColorIsNotSelected(hexValue: String) =
+ assertUIObjectExists(itemContainingText("Selected date is: $hexValue"), exists = false)
+
+ fun verifyCookieBannerExists(exists: Boolean) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "verifyCookieBannerExists: Started try #$i")
+ try {
+ // Wait for the blocker to kick-in and make the cookie banner disappear
+ Log.i(TAG, "verifyCookieBannerExists: Waiting for $waitingTime ms for cookie banner to be gone")
+ itemWithResId("CybotCookiebotDialog").waitUntilGone(waitingTime)
+ Log.i(TAG, "verifyCookieBannerExists: Waited for $waitingTime ms for cookie banner to be gone")
+ // Assert that the blocker properly dismissed the cookie banner
+ assertUIObjectExists(itemWithResId("CybotCookiebotDialog"), exists = exists)
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyCookieBannerExists: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ }
+ }
+ }
+ }
+
+ fun verifyCookieBannerBlockerCFRExists(exists: Boolean) =
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.cookie_banner_cfr_message)),
+ exists = exists,
+ )
+
+ fun verifyOpenLinkInAnotherAppPrompt() {
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/parentPanel"),
+ itemContainingText(
+ getStringResource(R.string.mozac_feature_applinks_normal_confirm_dialog_title),
+ ),
+ itemContainingText(
+ getStringResource(R.string.mozac_feature_applinks_normal_confirm_dialog_message),
+ ),
+ )
+ }
+
+ fun verifyPrivateBrowsingOpenLinkInAnotherAppPrompt(url: String, pageObject: UiObject) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifyPrivateBrowsingOpenLinkInAnotherAppPrompt: Started try #$i")
+ assertUIObjectExists(
+ itemContainingText(
+ getStringResource(R.string.mozac_feature_applinks_confirm_dialog_title),
+ ),
+ itemContainingText(url),
+ )
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyPrivateBrowsingOpenLinkInAnotherAppPrompt: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ clickPageObject(pageObject)
+ }
+ }
+ }
+ }
+ }
+
+ fun verifyFindInPageBar(exists: Boolean) =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/findInPageView"),
+ exists = exists,
+ )
+
+ fun verifyConnectionErrorMessage() =
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.mozac_browser_errorpages_connection_failure_title)),
+ itemWithResId("errorTryAgain"),
+ )
+
+ fun verifyAddressNotFoundErrorMessage() =
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.mozac_browser_errorpages_unknown_host_title)),
+ itemWithResId("errorTryAgain"),
+ )
+
+ fun verifyNoInternetConnectionErrorMessage() =
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.mozac_browser_errorpages_no_internet_title)),
+ itemWithResId("errorTryAgain"),
+ )
+
+ fun verifyOpenLinksInAppsCFRExists(exists: Boolean) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "verifyOpenLinksInAppsCFRExists: Started try #$i")
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/banner_container"),
+ itemWithResIdContainingText(
+ "$packageName:id/banner_info_message",
+ getStringResource(R.string.open_in_app_cfr_info_message_2),
+ ),
+ itemWithResIdContainingText(
+ "$packageName:id/dismiss",
+ getStringResource(R.string.open_in_app_cfr_negative_button_text),
+ ),
+ itemWithResIdContainingText(
+ "$packageName:id/action",
+ getStringResource(R.string.open_in_app_cfr_positive_button_text),
+ ),
+ exists = exists,
+ )
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyOpenLinksInAppsCFRExists: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }
+ }
+ }
+ }
+ }
+
+ fun verifySurveyButton() = assertUIObjectExists(itemContainingText(getStringResource(R.string.preferences_take_survey)))
+
+ fun verifySurveyButtonDoesNotExist() =
+ assertUIObjectIsGone(itemWithText(getStringResource(R.string.preferences_take_survey)))
+
+ fun verifySurveyNoThanksButton() =
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.preferences_not_take_survey)),
+ )
+
+ fun verifyHomeScreenSurveyCloseButton() =
+ assertUIObjectExists(itemWithDescription("Close"))
+
+ fun clickOpenLinksInAppsDismissCFRButton() {
+ Log.i(TAG, "clickOpenLinksInAppsDismissCFRButton: Trying to click the open links in apps banner \"Dismiss\" button")
+ itemWithResIdContainingText(
+ "$packageName:id/dismiss",
+ getStringResource(R.string.open_in_app_cfr_negative_button_text),
+ ).click()
+ Log.i(TAG, "clickOpenLinksInAppsDismissCFRButton: Clicked the open links in apps banner \"Dismiss\" button")
+ }
+
+ fun clickTakeSurveyButton() {
+ val button = mDevice.findObject(
+ UiSelector().text(
+ getStringResource(
+ R.string.preferences_take_survey,
+ ),
+ ),
+ )
+ button.waitForExists(waitingTime)
+ button.click()
+ }
+ fun clickNoThanksSurveyButton() {
+ val button = mDevice.findObject(
+ UiSelector().text(
+ getStringResource(
+ R.string.preferences_not_take_survey,
+ ),
+ ),
+ )
+ button.waitForExists(waitingTime)
+ button.click()
+ }
+
+ fun longClickToolbar() {
+ Log.i(TAG, "longClickToolbar: Trying to long click the toolbar")
+ onView(withId(R.id.mozac_browser_toolbar_url_view)).perform(longClick())
+ Log.i(TAG, "longClickToolbar: Long clicked the toolbar")
+ }
+
+ fun verifyDownloadPromptIsDismissed() =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/viewDynamicDownloadDialog"),
+ exists = false,
+ )
+
+ fun verifyCancelPrivateDownloadsPrompt(numberOfActiveDownloads: String) {
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/title",
+ getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_warning_content_title),
+ ),
+ itemWithResIdContainingText(
+ "$packageName:id/body",
+ "If you close all Private tabs now, $numberOfActiveDownloads download will be canceled. Are you sure you want to leave Private Browsing?",
+ ),
+ itemWithResIdContainingText(
+ "$packageName:id/deny_button",
+ getStringResource(R.string.mozac_feature_downloads_cancel_active_private_downloads_deny),
+ ),
+ itemWithResIdContainingText(
+ "$packageName:id/accept_button",
+ getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_accept),
+ ),
+ )
+ }
+
+ fun clickStayInPrivateBrowsingPromptButton() {
+ Log.i(TAG, "clickStayInPrivateBrowsingPromptButton: Trying to click the \"STAY IN PRIVATE BROWSING\" prompt button")
+ itemWithResIdContainingText(
+ "$packageName:id/deny_button",
+ getStringResource(R.string.mozac_feature_downloads_cancel_active_private_downloads_deny),
+ ).click()
+ Log.i(TAG, "clickStayInPrivateBrowsingPromptButton: Clicked the \"STAY IN PRIVATE BROWSING\" prompt button")
+ }
+
+ fun clickCancelPrivateDownloadsPromptButton() {
+ Log.i(TAG, "clickCancelPrivateDownloadsPromptButton: Trying to click the \"CANCEL DOWNLOADS\" prompt button")
+ itemWithResIdContainingText(
+ "$packageName:id/accept_button",
+ getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_accept),
+ ).click()
+ Log.i(TAG, "clickCancelPrivateDownloadsPromptButton: Clicked the \"CANCEL DOWNLOADS\" prompt button")
+ Log.i(TAG, "clickCancelPrivateDownloadsPromptButton: Waiting for $waitingTime ms for $packageName window to be updated")
+ mDevice.waitForWindowUpdate(packageName, waitingTime)
+ Log.i(TAG, "clickCancelPrivateDownloadsPromptButton: Waited for $waitingTime ms for $packageName window to be updated")
+ }
+
+ fun fillPdfForm(name: String) {
+ // Set PDF form text for the text box
+ Log.i(TAG, "fillPdfForm: Trying to set the text of the PDF form text box to: $name")
+ itemWithResId("pdfjs_internal_id_10R").setText(name)
+ Log.i(TAG, "fillPdfForm: PDF form text box text was set to: $name")
+ mDevice.waitForWindowUpdate(packageName, waitingTime)
+ if (
+ !itemWithResId("pdfjs_internal_id_11R").exists() &&
+ mDevice
+ .executeShellCommand("dumpsys input_method | grep mInputShown")
+ .contains("mInputShown=true")
+ ) {
+ // Close the keyboard
+ Log.i(TAG, "fillPdfForm: Trying to close the keyboard using device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "fillPdfForm: Closed the keyboard using device back button")
+ }
+ // Click PDF form check box
+ Log.i(TAG, "fillPdfForm: Trying to click the PDF form check box")
+ itemWithResId("pdfjs_internal_id_11R").click()
+ Log.i(TAG, "fillPdfForm: Clicked PDF form check box")
+ }
+
+ class Transition {
+ fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
+ Log.i(TAG, "openThreeDotMenu: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "openThreeDotMenu: Device was idle for $waitingTime ms")
+ Log.i(TAG, "openThreeDotMenu: Trying to click the main menu button")
+ threeDotButton().perform(click())
+ Log.i(TAG, "openThreeDotMenu: Clicked the main menu button")
+
+ ThreeDotMenuMainRobot().interact()
+ return ThreeDotMenuMainRobot.Transition()
+ }
+
+ fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
+ clickPageObject(navURLBar())
+ Log.i(TAG, "openNavigationToolbar: Waiting for $waitingTime ms for for search bar to exist")
+ searchBar().waitForExists(waitingTime)
+ Log.i(TAG, "openNavigationToolbar: Waited for $waitingTime ms for for search bar to exist")
+
+ NavigationToolbarRobot().interact()
+ return NavigationToolbarRobot.Transition()
+ }
+
+ fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "openTabDrawer: Started try #$i")
+ mDevice.waitForObjects(
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/mozac_browser_toolbar_browser_actions"),
+ ),
+ waitingTime,
+ )
+ Log.i(TAG, "openTabDrawer: Trying to click the tab counter button")
+ tabsCounter().click()
+ Log.i(TAG, "openTabDrawer: Clicked the tab counter button")
+ assertUIObjectExists(itemWithResId("$packageName:id/new_tab_button"))
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "openTabDrawer: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ Log.i(TAG, "openTabDrawer: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "openTabDrawer: Waited for device to be idle")
+ }
+ }
+ }
+
+ assertUIObjectExists(itemWithResId("$packageName:id/new_tab_button"))
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun openComposeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "openComposeTabDrawer: Started try #$i")
+ mDevice.waitForObjects(
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/mozac_browser_toolbar_browser_actions"),
+ ),
+ waitingTime,
+ )
+ Log.i(TAG, "openComposeTabDrawer: Trying to click the tab counter button")
+ tabsCounter().click()
+ Log.i(TAG, "openComposeTabDrawer: Clicked the tab counter button")
+ Log.i(TAG, "openComposeTabDrawer: Trying to verify the tabs tray exists")
+ composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
+ Log.i(TAG, "openComposeTabDrawer: Verified the tabs tray exists")
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "openComposeTabDrawer: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ Log.i(TAG, "openComposeTabDrawer: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "openComposeTabDrawer: Waited for device to be idle")
+ }
+ }
+ }
+ Log.i(TAG, "openComposeTabDrawer: Trying to verify the tabs tray new tab FAB button exists")
+ composeTestRule.onNodeWithTag(TabsTrayTestTag.fab).assertExists()
+ Log.i(TAG, "openComposeTabDrawer: Verified the tabs tray new tab FAB button exists")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun openNotificationShade(interact: NotificationRobot.() -> Unit): NotificationRobot.Transition {
+ Log.i(TAG, "openNotificationShade: Trying to open the notification tray")
+ mDevice.openNotification()
+ Log.i(TAG, "openNotificationShade: Opened the notification tray")
+
+ NotificationRobot().interact()
+ return NotificationRobot.Transition()
+ }
+
+ fun goToHomescreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ clickPageObject(itemWithDescription("Home screen"))
+ Log.i(TAG, "goToHomescreen: Waiting for $waitingTime ms for for home screen layout or jump back in contextual hint to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout"))
+ .waitForExists(waitingTime) ||
+ mDevice.findObject(
+ UiSelector().text(
+ getStringResource(R.string.onboarding_home_screen_jump_back_contextual_hint_2),
+ ),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "goToHomescreen: Waited for $waitingTime ms for for home screen layout or jump back in contextual hint to exist")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goToHomescreenWithComposeTopSites(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTopSitesRobot.() -> Unit): ComposeTopSitesRobot.Transition {
+ clickPageObject(itemWithDescription("Home screen"))
+
+ Log.i(TAG, "goToHomescreenWithComposeTopSites: Waiting for $waitingTime ms for for home screen layout or jump back in contextual hint to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout"))
+ .waitForExists(waitingTime) ||
+ mDevice.findObject(
+ UiSelector().text(
+ getStringResource(R.string.onboarding_home_screen_jump_back_contextual_hint_2),
+ ),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "goToHomescreenWithComposeTopSites: Waited for $waitingTime ms for for home screen layout or jump back in contextual hint to exist")
+
+ ComposeTopSitesRobot(composeTestRule).interact()
+ return ComposeTopSitesRobot.Transition(composeTestRule)
+ }
+
+ fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBack: Clicked device back button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun clickTabCrashedCloseButton(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ clickPageObject(itemWithText("Close tab"))
+ Log.i(TAG, "clickTabCrashedCloseButton: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "clickTabCrashedCloseButton: Waited for device to be idle")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun clickShareSelectedText(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ clickContextMenuItem("Share")
+
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+ }
+
+ fun clickDownloadLink(title: String, interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
+ clickPageObject(itemContainingText(title))
+
+ DownloadRobot().interact()
+ return DownloadRobot.Transition()
+ }
+
+ fun clickStartCameraButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Test page used for testing permissions located at https://mozilla-mobile.github.io/testapp/permissions
+ clickPageObject(itemWithText("Open camera"))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun clickStartMicrophoneButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Test page used for testing permissions located at https://mozilla-mobile.github.io/testapp/permissions
+ clickPageObject(itemWithText("Open microphone"))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun clickStartAudioVideoButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Test page used for testing permissions located at https://mozilla-mobile.github.io/testapp/permissions
+ clickPageObject(itemWithText("Camera & Microphone"))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun clickOpenNotificationButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Test page used for testing permissions located at https://mozilla-mobile.github.io/testapp/permissions
+ clickPageObject(itemWithText("Open notifications dialogue"))
+ mDevice.waitForObjects(mDevice.findObject(UiSelector().textContains("Allow to send notifications?")))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun clickGetLocationButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Test page used for testing permissions located at https://mozilla-mobile.github.io/testapp/permissions
+ clickPageObject(itemWithText("Get Location"))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun clickRequestStorageAccessButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Clicks the "request storage access" button from the "cross-site-cookies.html" local asset
+ clickPageObject(itemContainingText("requestStorageAccess()"))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun clickRequestPersistentStorageAccessButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Clicks the "Persistent storage" button from "https://mozilla-mobile.github.io/testapp/permissions"
+ clickPageObject(itemWithResId("persistentStorageButton"))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun clickRequestDRMControlledContentAccessButton(interact: SitePermissionsRobot.() -> Unit): SitePermissionsRobot.Transition {
+ // Clicks the "DRM-controlled content" button from "https://mozilla-mobile.github.io/testapp/permissions"
+ clickPageObject(itemWithResId("drmPermissionButton"))
+
+ SitePermissionsRobot().interact()
+ return SitePermissionsRobot.Transition()
+ }
+
+ fun openSiteSecuritySheet(interact: SiteSecurityRobot.() -> Unit): SiteSecurityRobot.Transition {
+ Log.i(TAG, "openSiteSecuritySheet: Waiting for $waitingTime ms for site security toolbar button to exist")
+ siteSecurityToolbarButton().waitForExists(waitingTime)
+ Log.i(TAG, "openSiteSecuritySheet: Waited for $waitingTime ms for site security toolbar button to exist")
+ Log.i(TAG, "openSiteSecuritySheet: Trying to click the site security toolbar button and wait for $waitingTime ms for a new window")
+ siteSecurityToolbarButton().clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "openSiteSecuritySheet: Clicked the site security toolbar button and waited for $waitingTime ms for a new window")
+
+ SiteSecurityRobot().interact()
+ return SiteSecurityRobot.Transition()
+ }
+
+ fun clickManageAddressButton(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
+ Log.i(TAG, "clickManageAddressButton: Trying to click the manage address button and wait for $waitingTime ms for a new window")
+ itemWithResId("$packageName:id/manage_addresses")
+ .clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickManageAddressButton: Clicked the manage address button and waited for $waitingTime ms for a new window")
+
+ SettingsSubMenuAutofillRobot().interact()
+ return SettingsSubMenuAutofillRobot.Transition()
+ }
+
+ fun clickManageCreditCardsButton(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
+ Log.i(TAG, "clickManageCreditCardsButton: Trying to click the manage credit cards button and wait for $waitingTime ms for a new window")
+ itemWithResId("$packageName:id/manage_credit_cards")
+ .clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickManageCreditCardsButton: Clicked the manage credit cards button and waited for $waitingTime ms for a new window")
+
+ SettingsSubMenuAutofillRobot().interact()
+ return SettingsSubMenuAutofillRobot.Transition()
+ }
+
+ fun clickOpenLinksInAppsGoToSettingsCFRButton(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Trying to click the \"Go to settings\" open links in apps CFR button and wait for $waitingTime ms for a new window")
+ itemWithResIdContainingText(
+ "$packageName:id/action",
+ getStringResource(R.string.open_in_app_cfr_positive_button_text),
+ ).clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Clicked the \"Go to settings\" open links in apps CFR button and waited for $waitingTime ms for a new window")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun clickDownloadPDFButton(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
+ Log.i(TAG, "clickDownloadPDFButton: Trying to click the download PDF button")
+ itemWithResIdContainingText(
+ "download",
+ "Download",
+ ).click()
+ Log.i(TAG, "clickDownloadPDFButton: Clicked the download PDF button")
+
+ DownloadRobot().interact()
+ return DownloadRobot.Transition()
+ }
+
+ fun clickSurveyButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ surveyButton().waitForExists(waitingTime)
+ surveyButton().click()
+
+ BrowserRobot().interact()
+ return Transition()
+ }
+
+ fun clickNoThanksSurveyButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ surveyNoThanksButton().waitForExists(waitingTime)
+ surveyNoThanksButton().click()
+
+ BrowserRobot().interact()
+ return Transition()
+ }
+
+ fun clickHomeScreenSurveyCloseButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ homescreenSurveyCloseButton().waitForExists(waitingTime)
+ homescreenSurveyCloseButton().click()
+
+ BrowserRobot().interact()
+ return Transition()
+ }
+ }
+}
+
+fun browserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+}
+
+private fun navURLBar() = itemWithResId("$packageName:id/toolbar")
+
+private fun searchBar() = itemWithResId("$packageName:id/mozac_browser_toolbar_url_view")
+
+private fun threeDotButton() = onView(withContentDescription("Menu"))
+
+private fun tabsCounter() =
+ mDevice.findObject(By.res("$packageName:id/counter_root"))
+
+private fun progressBar() =
+ itemWithResId("$packageName:id/mozac_browser_toolbar_progress")
+
+private fun suggestedLogins() = itemWithResId("$packageName:id/loginSelectBar")
+private fun selectAddressButton() = itemWithResId("$packageName:id/select_address_header")
+private fun selectCreditCardButton() = itemWithResId("$packageName:id/select_credit_card_header")
+
+private fun siteSecurityToolbarButton() =
+ itemWithResId("$packageName:id/mozac_browser_toolbar_security_indicator")
+
+fun clickPageObject(item: UiObject) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "clickPageObject: Started try #$i")
+ Log.i(TAG, "clickPageObject: Waiting for $waitingTime ms for ${item.selector} to exist")
+ item.waitForExists(waitingTime)
+ Log.i(TAG, "clickPageObject: Waited for $waitingTime ms for ${item.selector} to exist")
+ Log.i(TAG, "clickPageObject: Trying to click ${item.selector}")
+ item.click()
+ Log.i(TAG, "clickPageObject: Clicked ${item.selector}")
+
+ break
+ } catch (e: UiObjectNotFoundException) {
+ Log.i(TAG, "clickPageObject: UiObjectNotFoundException caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }
+ }
+ }
+ }
+}
+
+fun longClickPageObject(item: UiObject) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "longClickPageObject: Started try #$i")
+ Log.i(TAG, "longClickPageObject: Waiting for $waitingTime ms for ${item.selector} to exist")
+ item.waitForExists(waitingTime)
+ Log.i(TAG, "longClickPageObject: Waited for $waitingTime ms for ${item.selector} to exist")
+ Log.i(TAG, "longClickPageObject: Trying to long click ${item.selector}")
+ item.longClick()
+ Log.i(TAG, "longClickPageObject: Long clicked ${item.selector}")
+
+ break
+ } catch (e: UiObjectNotFoundException) {
+ Log.i(TAG, "longClickPageObject: UiObjectNotFoundException caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }
+ }
+ }
+ }
+}
+
+fun clickContextMenuItem(item: String) {
+ mDevice.waitNotNull(
+ Until.findObject(text(item)),
+ waitingTime,
+ )
+ Log.i(TAG, "clickContextMenuItem: Trying to click context menu item: $item")
+ mDevice.findObject(text(item)).click()
+ Log.i(TAG, "clickContextMenuItem: Clicked context menu item: $item")
+}
+
+fun setPageObjectText(webPageItem: UiObject, text: String) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "setPageObjectText: Started try #$i")
+ try {
+ webPageItem.also {
+ Log.i(TAG, "setPageObjectText: Waiting for $waitingTime ms for ${webPageItem.selector} to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "setPageObjectText: Waited for $waitingTime ms for ${webPageItem.selector} to exist")
+ Log.i(TAG, "setPageObjectText: Trying to clear ${webPageItem.selector} text field")
+ it.clearTextField()
+ Log.i(TAG, "setPageObjectText: Cleared ${webPageItem.selector} text field")
+ Log.i(TAG, "setPageObjectText: Trying to set ${webPageItem.selector} text to $text")
+ it.setText(text)
+ Log.i(TAG, "setPageObjectText: ${webPageItem.selector} text was set to $text")
+ }
+
+ break
+ } catch (e: UiObjectNotFoundException) {
+ Log.i(TAG, "setPageObjectText: UiObjectNotFoundException caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ waitForPageToLoad()
+ }
+ }
+ }
+ }
+}
+
+fun clearTextFieldItem(item: UiObject) {
+ Log.i(TAG, "clearTextFieldItem: Waiting for $waitingTime ms for ${item.selector} to exist")
+ item.waitForExists(waitingTime)
+ Log.i(TAG, "clearTextFieldItem: Waited for $waitingTime ms for ${item.selector} to exist")
+ Log.i(TAG, "clearTextFieldItem: Trying to clear ${item.selector} text field")
+ item.clearTextField()
+ Log.i(TAG, "clearTextFieldItem: Cleared ${item.selector} text field")
+}
+
+// Context menu items
+// Link URL
+private fun contextMenuLinkUrl(linkUrl: String) =
+ itemContainingText(linkUrl)
+
+// Open link in new tab option
+private fun contextMenuOpenLinkInNewTab() =
+ itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_new_tab))
+
+// Open link in private tab option
+private fun contextMenuOpenLinkInPrivateTab() =
+ itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_private_tab))
+
+// Copy link option
+private fun contextMenuCopyLink() =
+ itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_copy_link))
+
+// Download link option
+private fun contextMenuDownloadLink() =
+ itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_download_link))
+
+// Share link option
+private fun contextMenuShareLink() =
+ itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_share_link))
+
+// Open in external app option
+private fun contextMenuOpenInExternalApp() =
+ itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_external_app))
+
+private fun surveyButton() =
+ itemContainingText(getStringResource(R.string.preferences_take_survey))
+
+private fun surveyNoThanksButton() =
+ itemContainingText(getStringResource(R.string.preferences_not_take_survey))
+
+private fun homescreenSurveyCloseButton() =
+ itemWithDescription("Close")
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CollectionRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CollectionRobot.kt
new file mode 100644
index 0000000000..0b0b579be1
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CollectionRobot.kt
@@ -0,0 +1,315 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.pressImeActionButton
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+class CollectionRobot {
+
+ fun verifySelectCollectionScreen() =
+ assertUIObjectExists(
+ itemContainingText("Select collection"),
+ itemContainingText("Add new collection"),
+ itemWithResId("$packageName:id/collections_list"),
+ )
+
+ fun clickAddNewCollection() {
+ Log.i(TAG, "clickAddNewCollection: Trying to click the add new collection button")
+ addNewCollectionButton().click()
+ Log.i(TAG, "clickAddNewCollection: Clicked the add new collection button")
+ }
+
+ fun verifyCollectionNameTextField() = assertUIObjectExists(mainMenuEditCollectionNameField())
+
+ // names a collection saved from tab drawer
+ fun typeCollectionNameAndSave(collectionName: String) {
+ Log.i(TAG, "typeCollectionNameAndSave: Trying to set collection name text field to: $collectionName")
+ collectionNameTextField().setText(collectionName)
+ Log.i(TAG, "typeCollectionNameAndSave: Collection name text field set to: $collectionName")
+ Log.i(TAG, "typeCollectionNameAndSave: Waiting for $waitingTime ms for add collection button panel to exist")
+ addCollectionButtonPanel().waitForExists(waitingTime)
+ Log.i(TAG, "typeCollectionNameAndSave: Waited for $waitingTime ms for add collection button panel to exist")
+ Log.i(TAG, "typeCollectionNameAndSave: Trying to click \"OK\" panel button")
+ addCollectionOkButton().click()
+ Log.i(TAG, "typeCollectionNameAndSave: Clicked \"OK\" panel button")
+ }
+
+ fun verifyTabsSelectedCounterText(numOfTabs: Int) {
+ Log.i(TAG, "verifyTabsSelectedCounterText: Waiting for $waitingTime ms for \"Select tabs to save\" prompt to be gone")
+ itemWithText("Select tabs to save").waitUntilGone(waitingTime)
+ Log.i(TAG, "verifyTabsSelectedCounterText: Waited for $waitingTime ms for \"Select tabs to save\" prompt to be gone")
+
+ val tabsCounter = mDevice.findObject(UiSelector().resourceId("$packageName:id/bottom_bar_text"))
+ Log.i(TAG, "verifyTabsSelectedCounterText: Trying to assert that number of tabs selected is: $numOfTabs")
+ when (numOfTabs) {
+ 1 -> assertItemTextEquals(tabsCounter, expectedText = "$numOfTabs tab selected")
+ 2 -> assertItemTextEquals(tabsCounter, expectedText = "$numOfTabs tabs selected")
+ }
+ Log.i(TAG, "verifyTabsSelectedCounterText: Asserted number of tabs selected is: $numOfTabs")
+ }
+
+ fun saveTabsSelectedForCollection() {
+ Log.i(TAG, "saveTabsSelectedForCollection: Trying to click \"Save\" button")
+ itemWithResId("$packageName:id/save_button").click()
+ Log.i(TAG, "saveTabsSelectedForCollection: Clicked \"Save\" button")
+ }
+
+ fun verifyTabSavedInCollection(title: String, visible: Boolean = true) {
+ if (visible) {
+ scrollToElementByText(title)
+ assertUIObjectExists(collectionListItem(title))
+ } else {
+ assertUIObjectIsGone(collectionListItem(title))
+ }
+ }
+
+ fun verifyCollectionTabUrl(visible: Boolean, url: String) =
+ assertUIObjectExists(itemContainingText(url), exists = visible)
+
+ fun verifyShareCollectionButtonIsVisible(visible: Boolean) =
+ assertUIObjectExists(shareCollectionButton(), exists = visible)
+
+ fun verifyCollectionMenuIsVisible(visible: Boolean, rule: ComposeTestRule) {
+ if (visible) {
+ Log.i(TAG, "verifyCollectionMenuIsVisible: Trying to verify collection three dot button exists")
+ collectionThreeDotButton(rule).assertExists()
+ Log.i(TAG, "verifyCollectionMenuIsVisible: Verified collection three dot button exists")
+ Log.i(TAG, "verifyCollectionMenuIsVisible: Trying to verify collection three dot button is displayed")
+ collectionThreeDotButton(rule).assertIsDisplayed()
+ Log.i(TAG, "verifyCollectionMenuIsVisible: Verified collection three dot button is displayed")
+ } else {
+ Log.i(TAG, "verifyCollectionMenuIsVisible: Trying to verify collection three dot button does not exist")
+ collectionThreeDotButton(rule)
+ .assertDoesNotExist()
+ Log.i(TAG, "verifyCollectionMenuIsVisible: Verified collection three dot button does not exist")
+ }
+ }
+
+ fun clickCollectionThreeDotButton(rule: ComposeTestRule) {
+ Log.i(TAG, "clickCollectionThreeDotButton: Trying to verify three dot button is displayed")
+ collectionThreeDotButton(rule).assertIsDisplayed()
+ Log.i(TAG, "clickCollectionThreeDotButton: Verified three dot button is displayed")
+ Log.i(TAG, "clickCollectionThreeDotButton: Trying to click three dot button")
+ collectionThreeDotButton(rule).performClick()
+ Log.i(TAG, "clickCollectionThreeDotButton: Clicked three dot button")
+ }
+
+ fun selectOpenTabs(rule: ComposeTestRule) {
+ Log.i(TAG, "selectOpenTabs: Trying to verify \"Open tabs\" menu option is displayed")
+ rule.onNode(hasText("Open tabs")).assertIsDisplayed()
+ Log.i(TAG, "selectOpenTabs: Verified \"Open tabs\" menu option is displayed")
+ Log.i(TAG, "selectOpenTabs: Trying to click \"Open tabs\" menu option")
+ rule.onNode(hasText("Open tabs")).performClick()
+ Log.i(TAG, "selectOpenTabs: Clicked \"Open tabs\" menu option")
+ }
+
+ fun selectRenameCollection(rule: ComposeTestRule) {
+ Log.i(TAG, "selectRenameCollection: Trying to verify \"Rename collection\" menu option is displayed")
+ rule.onNode(hasText("Rename collection")).assertIsDisplayed()
+ Log.i(TAG, "selectRenameCollection: Verified \"Rename collection\" menu option is displayed")
+ Log.i(TAG, "selectRenameCollection: Trying to click \"Rename collection\" menu option")
+ rule.onNode(hasText("Rename collection")).performClick()
+ Log.i(TAG, "selectRenameCollection: Clicked \"Rename collection\" menu option")
+ Log.i(TAG, "selectRenameCollection: Waiting for $waitingTime ms for collection name text field to exist")
+ mainMenuEditCollectionNameField().waitForExists(waitingTime)
+ Log.i(TAG, "selectRenameCollection: Waited for $waitingTime ms for collection name text field to exist")
+ }
+
+ fun selectAddTabToCollection(rule: ComposeTestRule) {
+ Log.i(TAG, "selectAddTabToCollection: Trying to verify \"Add tab\" menu option is displayed")
+ rule.onNode(hasText("Add tab")).assertIsDisplayed()
+ Log.i(TAG, "selectAddTabToCollection: Verified \"Add tab\" menu option is displayed")
+ Log.i(TAG, "selectAddTabToCollection: Trying to click \"Add tab\" menu option")
+ rule.onNode(hasText("Add tab")).performClick()
+ Log.i(TAG, "selectAddTabToCollection: Clicked \"Add tab\" menu option")
+
+ mDevice.waitNotNull(Until.findObject(By.text("Select Tabs")))
+ }
+
+ fun selectDeleteCollection(rule: ComposeTestRule) {
+ Log.i(TAG, "selectDeleteCollection: Trying to verify \"Delete collection\" menu option is displayed")
+ rule.onNode(hasText("Delete collection")).assertIsDisplayed()
+ Log.i(TAG, "selectDeleteCollection: Verified \"Delete collection\" menu option is displayed")
+ Log.i(TAG, "selectDeleteCollection: Trying to click \"Delete collection\" menu option")
+ rule.onNode(hasText("Delete collection")).performClick()
+ Log.i(TAG, "selectDeleteCollection: Clicked \"Delete collection\" menu option")
+ }
+
+ fun verifyCollectionItemRemoveButtonIsVisible(title: String, visible: Boolean) =
+ assertUIObjectExists(removeTabFromCollectionButton(title), exists = visible)
+
+ fun removeTabFromCollection(title: String) {
+ Log.i(TAG, "removeTabFromCollection: Trying to click remove button for tab: $title")
+ removeTabFromCollectionButton(title).click()
+ Log.i(TAG, "removeTabFromCollection: Clicked remove button for tab: $title")
+ }
+
+ fun swipeTabLeft(title: String, rule: ComposeTestRule) {
+ Log.i(TAG, "swipeTabLeft: Trying to remove tab: $title using swipe left action")
+ rule.onNode(hasText(title), useUnmergedTree = true)
+ .performTouchInput { swipeLeft() }
+ Log.i(TAG, "swipeTabLeft: Removed tab: $title using swipe left action")
+ Log.i(TAG, "swipeTabLeft: Waiting for rule to be idle")
+ rule.waitForIdle()
+ Log.i(TAG, "swipeTabLeft: Waited for rule to be idle")
+ }
+
+ fun swipeTabRight(title: String, rule: ComposeTestRule) {
+ Log.i(TAG, "swipeTabRight: Trying to remove tab: $title using swipe right action")
+ rule.onNode(hasText(title), useUnmergedTree = true)
+ .performTouchInput { swipeRight() }
+ Log.i(TAG, "swipeTabRight: Removed tab: $title using swipe right action")
+ Log.i(TAG, "swipeTabRight: Waiting for rule to be idle")
+ rule.waitForIdle()
+ Log.i(TAG, "swipeTabRight: Waited for rule to be idle")
+ }
+
+ fun goBackInCollectionFlow() {
+ Log.i(TAG, "goBackInCollectionFlow: Trying to click collection creation flow back button")
+ backButton().click()
+ Log.i(TAG, "goBackInCollectionFlow: Clicked collection creation flow back button")
+ }
+
+ class Transition {
+ fun collapseCollection(
+ title: String,
+ interact: HomeScreenRobot.() -> Unit,
+ ): HomeScreenRobot.Transition {
+ assertUIObjectExists(itemContainingText(title))
+ Log.i(TAG, "collapseCollection: Trying to click collection $title and wait for $waitingTimeShort ms for a new window")
+ itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "collapseCollection: Clicked collection $title and waited for $waitingTimeShort ms for a new window")
+ assertUIObjectExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection)), exists = false)
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ // names a collection saved from the 3dot menu
+ fun typeCollectionNameAndSave(
+ name: String,
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ Log.i(TAG, "typeCollectionNameAndSave: Waiting for $waitingTime ms for collection name text field to exist")
+ mainMenuEditCollectionNameField().waitForExists(waitingTime)
+ Log.i(TAG, "typeCollectionNameAndSave: Waited for $waitingTime ms for collection name text field to exist")
+ Log.i(TAG, "typeCollectionNameAndSave: Trying to set collection name text field to: $name")
+ mainMenuEditCollectionNameField().setText(name)
+ Log.i(TAG, "typeCollectionNameAndSave: Collection name text field set to: $name")
+ Log.i(TAG, "typeCollectionNameAndSave: Trying to press done action button")
+ onView(withId(R.id.name_collection_edittext)).perform(pressImeActionButton())
+ Log.i(TAG, "typeCollectionNameAndSave: Pressed done action button")
+
+ // wait for the collection creation wrapper to be dismissed
+ mDevice.waitNotNull(Until.gone(By.res("$packageName:id/createCollectionWrapper")))
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun selectExistingCollection(
+ title: String,
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ Log.i(TAG, "selectExistingCollection: Waiting for $waitingTime ms for collection with title: $title to exist")
+ collectionTitle(title).waitForExists(waitingTime)
+ Log.i(TAG, "selectExistingCollection: Waited for $waitingTime ms for collection with title: $title to exist")
+ Log.i(TAG, "selectExistingCollection: Trying to click collection with title: $title")
+ collectionTitle(title).click()
+ Log.i(TAG, "selectExistingCollection: Clicked collection with title: $title")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickShareCollectionButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ Log.i(TAG, "clickShareCollectionButton: Waiting for $waitingTime ms for share collection button to exist")
+ shareCollectionButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickShareCollectionButton: Waited for $waitingTime ms for share collection button to exist")
+ Log.i(TAG, "clickShareCollectionButton: Trying to click share collection button")
+ shareCollectionButton().click()
+ Log.i(TAG, "clickShareCollectionButton: Clicked share collection button")
+
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+ }
+ }
+}
+
+fun collectionRobot(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
+ CollectionRobot().interact()
+ return CollectionRobot.Transition()
+}
+
+private fun collectionTitle(title: String) = itemWithText(title)
+
+private fun collectionThreeDotButton(rule: ComposeTestRule) =
+ rule.onNode(hasContentDescription("Collection menu"))
+
+private fun collectionListItem(title: String) = mDevice.findObject(UiSelector().text(title))
+
+private fun shareCollectionButton() = itemWithDescription("Share")
+
+private fun removeTabFromCollectionButton(title: String) =
+ mDevice.findObject(
+ UiSelector().text(title),
+ ).getFromParent(
+ UiSelector()
+ .description("Remove tab from collection"),
+ )
+
+// collection name text field, opened from tab drawer
+private fun collectionNameTextField() =
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/collection_name"),
+ )
+
+// collection name text field, when saving from the main menu option
+private fun mainMenuEditCollectionNameField() =
+ itemWithResId("$packageName:id/name_collection_edittext")
+
+private fun addNewCollectionButton() =
+ mDevice.findObject(UiSelector().text("Add new collection"))
+
+private fun backButton() =
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/back_button"),
+ )
+private fun addCollectionButtonPanel() =
+ itemWithResId("$packageName:id/buttonPanel")
+
+private fun addCollectionOkButton() = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt
new file mode 100644
index 0000000000..32763810eb
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt
@@ -0,0 +1,771 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import android.view.View
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.filter
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasParent
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onChildAt
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.action.GeneralLocation
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import org.hamcrest.Matcher
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.clickAtLocationInView
+import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource
+import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher
+import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorStateMatcher
+import org.mozilla.fenix.tabstray.TabsTrayTestTag
+
+/**
+ * Implementation of Robot Pattern for the Tabs Tray.
+ */
+class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTestRule) {
+
+ fun verifyNormalBrowsingButtonIsSelected(isSelected: Boolean = true) {
+ if (isSelected) {
+ Log.i(TAG, "verifyNormalBrowsingButtonIsSelected: Trying to verify that the normal browsing button is selected")
+ composeTestRule.normalBrowsingButton().assertIsSelected()
+ Log.i(TAG, "verifyNormalBrowsingButtonIsSelected: Verified that the normal browsing button is selected")
+ } else {
+ Log.i(TAG, "verifyNormalBrowsingButtonIsSelected: Trying to verify that the normal browsing button is not selected")
+ composeTestRule.normalBrowsingButton().assertIsNotSelected()
+ Log.i(TAG, "verifyNormalBrowsingButtonIsSelected: Verified that the normal browsing button is not selected")
+ }
+ }
+
+ fun verifyPrivateBrowsingButtonIsSelected(isSelected: Boolean = true) {
+ if (isSelected) {
+ Log.i(TAG, "verifyPrivateBrowsingButtonIsSelected: Trying to verify that the private browsing button is selected")
+ composeTestRule.privateBrowsingButton().assertIsSelected()
+ Log.i(TAG, "verifyPrivateBrowsingButtonIsSelected: Verified that the private browsing button is selected")
+ } else {
+ Log.i(TAG, "verifyPrivateBrowsingButtonIsSelected: Trying to verify that the private browsing button is not selected")
+ composeTestRule.privateBrowsingButton().assertIsNotSelected()
+ Log.i(TAG, "verifyPrivateBrowsingButtonIsSelected: Verified that the private browsing button is not selected")
+ }
+ }
+
+ fun verifySyncedTabsButtonIsSelected(isSelected: Boolean = true) {
+ if (isSelected) {
+ Log.i(TAG, "verifySyncedTabsButtonIsSelected: Trying to verify that the synced tabs button is selected")
+ composeTestRule.syncedTabsButton().assertIsSelected()
+ Log.i(TAG, "verifySyncedTabsButtonIsSelected: Verified that the synced tabs button is selected")
+ } else {
+ Log.i(TAG, "verifySyncedTabsButtonIsSelected: Trying to verify that the synced tabs button is not selected")
+ composeTestRule.syncedTabsButton().assertIsNotSelected()
+ Log.i(TAG, "verifySyncedTabsButtonIsSelected: Verified that the synced tabs button is not selected")
+ }
+ }
+
+ fun verifySyncedTabsListWhenUserIsNotSignedIn() {
+ verifySyncedTabsList()
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.synced_tabs_sign_in_message)),
+ itemContainingText(getStringResource(R.string.sync_sign_in)),
+ itemContainingText(getStringResource(R.string.tab_drawer_fab_sync)),
+ )
+ }
+
+ fun verifyExistingOpenTabs(vararg titles: String) {
+ titles.forEach { title ->
+ Log.i(TAG, "verifyExistingOpenTabs: Waiting for $waitingTime ms for tab with title: $title to exist")
+ itemContainingText(title).waitForExists(waitingTime)
+ Log.i(TAG, "verifyExistingOpenTabs: Waited for $waitingTime ms for tab with title: $title to exist")
+ Log.i(TAG, "verifyExistingOpenTabs: Trying to verify that the open tab with title: $title exists")
+ composeTestRule.tabItem(title).assertExists()
+ Log.i(TAG, "verifyExistingOpenTabs: Verified that the open tab with title: $title exists")
+ }
+ }
+
+ fun verifyOpenTabsOrder(title: String, position: Int) {
+ Log.i(TAG, "verifyOpenTabsOrder: Trying to verify that the open tab at position: $position has title: $title")
+ composeTestRule.normalTabsList()
+ .onChildAt(position - 1)
+ .assert(hasTestTag(TabsTrayTestTag.tabItemRoot))
+ .assert(hasAnyChild(hasText(title)))
+ Log.i(TAG, "verifyOpenTabsOrder: Verified that the open tab at position: $position has title: $title")
+ }
+
+ fun verifyNoExistingOpenTabs(vararg titles: String) {
+ titles.forEach { title ->
+ assertUIObjectExists(
+ itemContainingText(title),
+ exists = false,
+ )
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ fun verifyNormalTabsList() {
+ composeTestRule.waitUntilDoesNotExist(hasTestTag("tabstray.tabList.normal.empty"), waitingTime)
+ Log.i(TAG, "verifyNormalTabsList: Trying to verify that the normal tabs list exists")
+ composeTestRule.normalTabsList().assertExists()
+ Log.i(TAG, "verifyNormalTabsList: Verified that the normal tabs list exists")
+ }
+
+ fun verifyPrivateTabsList() {
+ Log.i(TAG, "verifyPrivateTabsList: Trying to verify that the private tabs list exists")
+ composeTestRule.privateTabsList().assertExists()
+ Log.i(TAG, "verifyPrivateTabsList: Verified that the private tabs list exists")
+ }
+
+ fun verifySyncedTabsList() {
+ Log.i(TAG, "verifySyncedTabsList: Trying to verify that the synced tabs list exists")
+ composeTestRule.syncedTabsList().assertExists()
+ Log.i(TAG, "verifySyncedTabsList: Verified that the synced tabs list exists")
+ }
+
+ fun verifyNoOpenTabsInNormalBrowsing() {
+ Log.i(TAG, "verifyNoOpenTabsInNormalBrowsing: Trying to verify that the empty normal tabs list exists")
+ composeTestRule.emptyNormalTabsList().assertExists()
+ Log.i(TAG, "verifyNoOpenTabsInNormalBrowsing: Verified that the empty normal tabs list exists")
+ }
+
+ fun verifyNoOpenTabsInPrivateBrowsing() {
+ Log.i(TAG, "verifyNoOpenTabsInPrivateBrowsing: Trying to verify that the empty private tabs list exists")
+ composeTestRule.emptyPrivateTabsList().assertExists()
+ Log.i(TAG, "verifyNoOpenTabsInPrivateBrowsing: Verified that the empty private tabs list exists")
+ }
+
+ fun verifyAccountSettingsButton() {
+ Log.i(TAG, "verifyAccountSettingsButton: Trying to verify that the \"Account settings\" menu button exists")
+ composeTestRule.dropdownMenuItemAccountSettings().assertExists()
+ Log.i(TAG, "verifyAccountSettingsButton: Verified that the \"Account settings\" menu button exists")
+ }
+
+ fun verifyCloseAllTabsButton() {
+ Log.i(TAG, "verifyCloseAllTabsButton: Trying to verify that the \"Close all tabs\" menu button exists")
+ composeTestRule.dropdownMenuItemCloseAllTabs().assertExists()
+ Log.i(TAG, "verifyCloseAllTabsButton: Verified that the \"Close all tabs\" menu button exists")
+ }
+
+ fun verifySelectTabsButton() {
+ Log.i(TAG, "verifySelectTabsButton: Trying to verify that the \"Select tabs\" menu button exists")
+ composeTestRule.dropdownMenuItemSelectTabs().assertExists()
+ Log.i(TAG, "verifySelectTabsButton: Verified that the \"Select tabs\" menu button exists")
+ }
+
+ fun verifyShareAllTabsButton() {
+ Log.i(TAG, "verifyShareAllTabsButton: Trying to verify that the \"Share all tabs\" menu button exists")
+ composeTestRule.dropdownMenuItemShareAllTabs().assertExists()
+ Log.i(TAG, "verifyShareAllTabsButton: Verified that the \"Share all tabs\" menu button exists")
+ }
+
+ fun verifyRecentlyClosedTabsButton() {
+ Log.i(TAG, "verifyRecentlyClosedTabsButton: Trying to verify that the \"Recently closed tabs\" menu button exists")
+ composeTestRule.dropdownMenuItemRecentlyClosedTabs().assertExists()
+ Log.i(TAG, "verifyRecentlyClosedTabsButton: Verified that the \"Recently closed tabs\" menu button exists")
+ }
+
+ fun verifyTabSettingsButton() {
+ Log.i(TAG, "verifyTabSettingsButton: Trying to verify that the \"Tab settings\" menu button exists")
+ composeTestRule.dropdownMenuItemTabSettings().assertExists()
+ Log.i(TAG, "verifyTabSettingsButton: Verified that the \"Tab settings\" menu button exists")
+ }
+
+ fun verifyThreeDotButton() {
+ Log.i(TAG, "verifyThreeDotButton: Trying to verify that the three dot button exists")
+ composeTestRule.threeDotButton().assertExists()
+ Log.i(TAG, "verifyThreeDotButton: Verified that the three dot button exists")
+ }
+
+ fun verifyFab() {
+ Log.i(TAG, "verifyFab: Trying to verify that the new tab FAB button exists")
+ composeTestRule.tabsTrayFab().assertExists()
+ Log.i(TAG, "verifyFab: Verified that the new tab FAB button exists")
+ }
+
+ fun verifyNormalTabCounter() {
+ Log.i(TAG, "verifyNormalTabCounter: Trying to verify that the normal tabs list counter exists")
+ composeTestRule.normalTabsCounter().assertExists()
+ Log.i(TAG, "verifyNormalTabCounter: Verified that the normal tabs list counter exists")
+ }
+
+ /**
+ * Verifies a tab's thumbnail when there is only one tab open.
+ */
+ fun verifyTabThumbnail() {
+ Log.i(TAG, "verifyTabThumbnail: Trying to verify that the tab thumbnail exists")
+ composeTestRule.tabThumbnail().assertExists()
+ Log.i(TAG, "verifyTabThumbnail: Verified that the tab thumbnail exists")
+ }
+
+ /**
+ * Verifies a tab's close button when there is only one tab open.
+ */
+ fun verifyTabCloseButton() {
+ Log.i(TAG, "verifyTabCloseButton: Trying to verify that the close tab button exists")
+ composeTestRule.closeTabButton().assertExists()
+ Log.i(TAG, "verifyTabCloseButton: Verified that the close tab button exists")
+ }
+
+ fun verifyTabsTrayBehaviorState(expectedState: Int) {
+ Log.i(TAG, "verifyTabsTrayBehaviorState: Trying to verify that the tabs tray state matches: $expectedState")
+ tabsTrayView().check(ViewAssertions.matches(BottomSheetBehaviorStateMatcher(expectedState)))
+ Log.i(TAG, "verifyTabsTrayBehaviorState: Verified that the tabs tray state matches: $expectedState")
+ }
+
+ fun verifyMinusculeHalfExpandedRatio() {
+ Log.i(TAG, "verifyMinusculeHalfExpandedRatio: Trying to verify the tabs tray half expanded ratio")
+ tabsTrayView().check(ViewAssertions.matches(BottomSheetBehaviorHalfExpandedMaxRatioMatcher(0.001f)))
+ Log.i(TAG, "verifyMinusculeHalfExpandedRatio: Verified the tabs tray half expanded ratio")
+ }
+
+ fun verifyTabTrayIsOpen() {
+ Log.i(TAG, "verifyTabTrayIsOpen: Trying to verify that the tabs tray exists")
+ composeTestRule.tabsTray().assertExists()
+ Log.i(TAG, "verifyTabTrayIsOpen: Verified that the tabs tray exists")
+ }
+
+ fun verifyTabTrayIsClosed() {
+ Log.i(TAG, "verifyTabTrayIsClosed: Trying to verify that the tabs tray does not exist")
+ composeTestRule.tabsTray().assertDoesNotExist()
+ Log.i(TAG, "verifyTabTrayIsClosed: Verified that the tabs tray does not exist")
+ }
+
+ /**
+ * Closes a tab when there is only one tab open.
+ */
+ @OptIn(ExperimentalTestApi::class)
+ fun closeTab() {
+ Log.i(TAG, "closeTab: Waiting until the close tab button exists")
+ composeTestRule.waitUntilAtLeastOneExists(hasTestTag(TabsTrayTestTag.tabItemClose))
+ Log.i(TAG, "closeTab: Waited until the close tab button exists")
+ Log.i(TAG, "closeTab: Trying to verify that the close tab button exists")
+ composeTestRule.closeTabButton().assertExists()
+ Log.i(TAG, "closeTab: Verified that the close tab button exists")
+ Log.i(TAG, "closeTab: Trying to click the close tab button")
+ composeTestRule.closeTabButton().performClick()
+ Log.i(TAG, "closeTab: Clicked the close tab button")
+ }
+
+ /**
+ * Swipes a tab with [title] left.
+ */
+ fun swipeTabLeft(title: String) {
+ Log.i(TAG, "swipeTabLeft: Trying to perform swipe left action on tab: $title")
+ composeTestRule.tabItem(title).performTouchInput { swipeLeft() }
+ Log.i(TAG, "swipeTabLeft: Performed swipe left action on tab: $title")
+ Log.i(TAG, "swipeTabLeft: Waiting for compose test rule to be idle")
+ composeTestRule.waitForIdle()
+ Log.i(TAG, "swipeTabLeft: Waited for compose test rule to be idle")
+ }
+
+ /**
+ * Swipes a tab with [title] right.
+ */
+ fun swipeTabRight(title: String) {
+ Log.i(TAG, "swipeTabRight: Trying to perform swipe right action on tab: $title")
+ composeTestRule.tabItem(title).performTouchInput { swipeRight() }
+ Log.i(TAG, "swipeTabRight: Performed swipe right action on tab: $title")
+ Log.i(TAG, "swipeTabRight: Waiting for compose test rule to be idle")
+ composeTestRule.waitForIdle()
+ Log.i(TAG, "swipeTabRight: Waited for compose test rule to be idle")
+ }
+
+ /**
+ * Creates a collection from the provided [tabTitles].
+ */
+ fun createCollection(
+ vararg tabTitles: String,
+ collectionName: String,
+ firstCollection: Boolean = true,
+ ) {
+ Log.i(TAG, "createCollection: Trying to click the three dot button")
+ composeTestRule.threeDotButton().performClick()
+ Log.i(TAG, "createCollection: Clicked the three dot button")
+ Log.i(TAG, "createCollection: Trying to click the \"Select tabs\" menu button")
+ composeTestRule.dropdownMenuItemSelectTabs().performClick()
+ Log.i(TAG, "createCollection: Clicked the \"Select tabs\" menu button")
+
+ for (tab in tabTitles) {
+ selectTab(tab, numberOfSelectedTabs = tabTitles.indexOf(tab) + 1)
+ }
+
+ clickCollectionsButton(composeTestRule) {
+ if (!firstCollection) {
+ clickAddNewCollection()
+ }
+ typeCollectionNameAndSave(collectionName)
+ }
+ }
+
+ /**
+ * Selects a tab with [title].
+ */
+ @OptIn(ExperimentalTestApi::class)
+ fun selectTab(title: String, numberOfSelectedTabs: Int = 0) {
+ Log.i(TAG, "selectTab: Waiting for $waitingTime ms until the tab with title: $title exists")
+ composeTestRule.waitUntilExactlyOneExists(hasText(title), waitingTime)
+ Log.i(TAG, "selectTab: Waited for $waitingTime ms until the tab with title: $title exists")
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "selectTab: Trying to click tab with title: $title")
+ composeTestRule.tabItem(title).performClick()
+ Log.i(TAG, "selectTab: Clicked tab with title: $title")
+ verifyTabsMultiSelectionCounter(numberOfSelectedTabs)
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "selectTab: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ // Dismiss tab selection
+ Log.i(TAG, "selectTab: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "selectTab: Clicked the device back button")
+ // Reopen tab selection section
+ Log.i(TAG, "selectTab: Trying to click the three dot button")
+ composeTestRule.threeDotButton().performClick()
+ Log.i(TAG, "selectTab: Clicked the three dot button")
+ Log.i(TAG, "selectTab: Trying to click the \"Select tabs\" menu button")
+ composeTestRule.dropdownMenuItemSelectTabs().performClick()
+ Log.i(TAG, "selectTab: Clicked the \"Select tabs\" menu button")
+ }
+ }
+ }
+ }
+
+ /**
+ * Performs a long click on a tab with [title].
+ */
+ fun longClickTab(title: String) {
+ Log.i(TAG, "longClickTab: Trying to long click tab with title: $title")
+ composeTestRule.tabItem(title)
+ .performTouchInput { longClick(durationMillis = Constants.LONG_CLICK_DURATION) }
+ Log.i(TAG, "longClickTab: Long clicked tab with title: $title")
+ }
+
+ /**
+ * Verifies the multi selection counter displays [numOfTabs].
+ */
+ fun verifyTabsMultiSelectionCounter(numOfTabs: Int) {
+ Log.i(TAG, "verifyTabsMultiSelectionCounter: Trying to verify that $numOfTabs tabs are selected")
+ composeTestRule.multiSelectionCounter()
+ .assert(hasText("$numOfTabs selected"))
+ Log.i(TAG, "verifyTabsMultiSelectionCounter: Verified that $numOfTabs tabs are selected")
+ }
+
+ /**
+ * Verifies a tab's media button matches [action] when there is only one tab with media.
+ */
+ @OptIn(ExperimentalTestApi::class)
+ fun verifyTabMediaControlButtonState(action: String) {
+ Log.i(TAG, "verifyTabMediaControlButtonStateTab: Waiting for $waitingTime ms until the media tab control button: $action exists")
+ composeTestRule.waitUntilAtLeastOneExists(hasContentDescription(action), waitingTime)
+ Log.i(TAG, "verifyTabMediaControlButtonStateTab: Waited for $waitingTime ms until the media tab control button: $action exists")
+ Log.i(TAG, "verifyTabMediaControlButtonStateTab: Trying to verify that the tab media control button: $action exists")
+ composeTestRule.tabMediaControlButton(action)
+ .assertExists()
+ Log.i(TAG, "verifyTabMediaControlButtonStateTab: Verified tab media control button: $action exists")
+ }
+
+ /**
+ * Clicks a tab's media button when there is only one tab with media.
+ */
+ @OptIn(ExperimentalTestApi::class)
+ fun clickTabMediaControlButton(action: String) {
+ Log.i(TAG, "clickTabMediaControlButton: Waiting for $waitingTime ms until the media tab control button: $action exists")
+ composeTestRule.waitUntilAtLeastOneExists(hasContentDescription(action), waitingTime)
+ Log.i(TAG, "clickTabMediaControlButton: Waited for $waitingTime ms until the media tab control button: $action exists")
+ Log.i(TAG, "clickTabMediaControlButton: Trying to click the tab media control button: $action")
+ composeTestRule.tabMediaControlButton(action)
+ .performClick()
+ Log.i(TAG, "clickTabMediaControlButton: Clicked the tab media control button: $action")
+ }
+
+ /**
+ * Closes a tab with a given [title].
+ */
+ fun closeTabWithTitle(title: String) {
+ Log.i(TAG, "closeTabWithTitle: Trying to click the close button for tab with title: $title")
+ composeTestRule.onAllNodesWithTag(TabsTrayTestTag.tabItemClose)
+ .filter(hasParent(hasText(title)))
+ .onFirst()
+ .performClick()
+ Log.i(TAG, "closeTabWithTitle: Clicked the close button for tab with title: $title")
+ }
+
+ class Transition(private val composeTestRule: HomeActivityComposeTestRule) {
+
+ fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "openNewTab: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "openNewTab: Waited for device to be idle")
+ Log.i(TAG, "openNewTab: Trying to click the new tab FAB button")
+ composeTestRule.tabsTrayFab().performClick()
+ Log.i(TAG, "openNewTab: Clicked the new tab FAB button")
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+
+ fun toggleToNormalTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "toggleToNormalTabs: Trying to click the normal browsing button")
+ composeTestRule.normalBrowsingButton().performClick()
+ Log.i(TAG, "toggleToNormalTabs: Clicked the normal browsing button")
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ fun toggleToPrivateTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "toggleToPrivateTabs: Trying to click the private browsing button")
+ composeTestRule.privateBrowsingButton().performClick()
+ Log.i(TAG, "toggleToPrivateTabs: Clicked the private browsing button")
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ fun toggleToSyncedTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "toggleToSyncedTabs: Trying to click the synced tabs button")
+ composeTestRule.syncedTabsButton().performClick()
+ Log.i(TAG, "toggleToSyncedTabs: Clicked the synced tabs button")
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ fun clickSignInToSyncButton(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
+ Log.i(TAG, "clickSignInToSyncButton: Trying to click the sign in to sync button and wait for $waitingTimeShort ms for a new window")
+ itemContainingText(getStringResource(R.string.sync_sign_in))
+ .clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "clickSignInToSyncButton: Clicked the sign in to sync button and waited for $waitingTimeShort ms for a new window")
+ SyncSignInRobot().interact()
+ return SyncSignInRobot.Transition()
+ }
+
+ fun openThreeDotMenu(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "openThreeDotMenu: Trying to click the three dot button")
+ composeTestRule.threeDotButton().performClick()
+ Log.i(TAG, "openThreeDotMenu: Clicked three dot button")
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ fun closeAllTabs(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "closeAllTabs: Trying to click the \"Close all tabs\" menu button")
+ composeTestRule.dropdownMenuItemCloseAllTabs().performClick()
+ Log.i(TAG, "closeAllTabs: Clicked the \"Close all tabs\" menu button")
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openTab: Trying to scroll to tab with title: $title")
+ composeTestRule.tabItem(title).performScrollTo()
+ Log.i(TAG, "openTab: Scrolled to tab with title: $title")
+ Log.i(TAG, "openTab: Trying to click tab with title: $title")
+ composeTestRule.tabItem(title).performClick()
+ Log.i(TAG, "openTab: Clicked tab with title: $title")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openPrivateTab(position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openPrivateTab: Trying to click private tab at position: ${position + 1}")
+ composeTestRule.privateTabsList()
+ .onChildren()[position]
+ .performClick()
+ Log.i(TAG, "openPrivateTab: Clicked private tab at position: ${position + 1}")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openNormalTab(position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openNormalTab: Trying to click tab at position: ${position + 1}")
+ composeTestRule.normalTabsList()
+ .onChildren()[position]
+ .performClick()
+ Log.i(TAG, "openNormalTab: Clicked tab at position: ${position + 1}")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickTopBar(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
+ // The topBar contains other views.
+ // Don't do the default click in the middle, rather click in some free space - top right.
+ Log.i(TAG, "clickTopBar: Trying to click the tabs tray top bar")
+ Espresso.onView(ViewMatchers.withId(R.id.topBar)).clickAtLocationInView(GeneralLocation.TOP_RIGHT)
+ Log.i(TAG, "clickTopBar: Clicked the tabs tray top bar")
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ fun waitForTabTrayBehaviorToIdle(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
+ // Need to get the behavior of tab_wrapper and wait for that to idle.
+ var behavior: BottomSheetBehavior<*>? = null
+
+ // Null check here since it's possible that the view is already animated away from the screen.
+ tabsTrayView()?.perform(
+ object : ViewAction {
+ override fun getDescription(): String {
+ return "Postpone actions to after the BottomSheetBehavior has settled"
+ }
+
+ override fun getConstraints(): Matcher {
+ return ViewMatchers.isAssignableFrom(View::class.java)
+ }
+
+ override fun perform(uiController: UiController?, view: View?) {
+ behavior = BottomSheetBehavior.from(view!!)
+ }
+ },
+ )
+
+ behavior?.let {
+ registerAndCleanupIdlingResources(
+ BottomSheetBehaviorStateIdlingResource(it),
+ ) {
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ }
+ }
+
+ return Transition(composeTestRule)
+ }
+
+ fun advanceToHalfExpandedState(interact: ComposeTabDrawerRobot.() -> Unit): Transition {
+ tabsTrayView().perform(
+ object : ViewAction {
+ override fun getDescription(): String {
+ return "Advance a BottomSheetBehavior to STATE_HALF_EXPANDED"
+ }
+
+ override fun getConstraints(): Matcher {
+ return ViewMatchers.isAssignableFrom(View::class.java)
+ }
+
+ override fun perform(uiController: UiController?, view: View?) {
+ val behavior = BottomSheetBehavior.from(view!!)
+ behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
+ }
+ },
+ )
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeTabDrawer: Trying to close the tabs tray by clicking the handle")
+ composeTestRule.bannerHandle().performSemanticsAction(SemanticsActions.OnClick)
+ Log.i(TAG, "closeTabDrawer: Closed the tabs tray by clicking the handle")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickSaveCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
+ Log.i(TAG, "clickSaveCollection: Trying to click the collections button")
+ composeTestRule.collectionsButton().performClick()
+ Log.i(TAG, "clickSaveCollection: Clicked collections button")
+
+ CollectionRobot().interact()
+ return CollectionRobot.Transition()
+ }
+
+ fun clickShareAllTabsButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ Log.i(TAG, "clickShareAllTabsButton: Trying to click the \"Share all tabs\" menu button button")
+ composeTestRule.dropdownMenuItemShareAllTabs().performClick()
+ Log.i(TAG, "clickShareAllTabsButton: Clicked the \"Share all tabs\" menu button button")
+
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+ }
+ }
+}
+
+/**
+ * Opens a transition in the [ComposeTabDrawerRobot].
+ */
+fun composeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+}
+
+/**
+ * Clicks on the Collections button in the Tabs Tray banner and opens a transition in the [CollectionRobot].
+ */
+private fun clickCollectionsButton(composeTestRule: HomeActivityComposeTestRule, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
+ Log.i(TAG, "clickCollectionsButton: Trying to click the collections button")
+ composeTestRule.collectionsButton().performClick()
+ Log.i(TAG, "clickCollectionsButton: Clicked the collections button")
+
+ CollectionRobot().interact()
+ return CollectionRobot.Transition()
+}
+
+/**
+ * Obtains the root [View] that wraps the Tabs Tray.
+ */
+private fun tabsTrayView() = Espresso.onView(ViewMatchers.withId(R.id.tabs_tray_root))
+
+/**
+ * Obtains the root Tabs Tray.
+ */
+private fun ComposeTestRule.tabsTray() = onNodeWithTag(TabsTrayTestTag.tabsTray)
+
+/**
+ * Obtains the Tabs Tray FAB.
+ */
+private fun ComposeTestRule.tabsTrayFab() = onNodeWithTag(TabsTrayTestTag.fab)
+
+/**
+ * Obtains the normal browsing page button of the Tabs Tray banner.
+ */
+private fun ComposeTestRule.normalBrowsingButton() = onNodeWithTag(TabsTrayTestTag.normalTabsPageButton)
+
+/**
+ * Obtains the private browsing page button of the Tabs Tray banner.
+ */
+private fun ComposeTestRule.privateBrowsingButton() = onNodeWithTag(TabsTrayTestTag.privateTabsPageButton)
+
+/**
+ * Obtains the synced tabs page button of the Tabs Tray banner.
+ */
+private fun ComposeTestRule.syncedTabsButton() = onNodeWithTag(TabsTrayTestTag.syncedTabsPageButton)
+
+/**
+ * Obtains the normal tabs list.
+ */
+private fun ComposeTestRule.normalTabsList() = onNodeWithTag(TabsTrayTestTag.normalTabsList)
+
+/**
+ * Obtains the private tabs list.
+ */
+private fun ComposeTestRule.privateTabsList() = onNodeWithTag(TabsTrayTestTag.privateTabsList)
+
+/**
+ * Obtains the synced tabs list.
+ */
+private fun ComposeTestRule.syncedTabsList() = onNodeWithTag(TabsTrayTestTag.syncedTabsList)
+
+/**
+ * Obtains the empty normal tabs list.
+ */
+private fun ComposeTestRule.emptyNormalTabsList() = onNodeWithTag(TabsTrayTestTag.emptyNormalTabsList)
+
+/**
+ * Obtains the empty private tabs list.
+ */
+private fun ComposeTestRule.emptyPrivateTabsList() = onNodeWithTag(TabsTrayTestTag.emptyPrivateTabsList)
+
+/**
+ * Obtains the tab with the provided [title]
+ */
+private fun ComposeTestRule.tabItem(title: String) = onAllNodesWithTag(TabsTrayTestTag.tabItemRoot)
+ .filter(hasAnyChild(hasText(title)))
+ .onFirst()
+
+/**
+ * Obtains an open tab's close button when there's only one tab open.
+ */
+private fun ComposeTestRule.closeTabButton() = onNodeWithTag(TabsTrayTestTag.tabItemClose)
+
+/**
+ * Obtains an open tab's thumbnail when there's only one tab open.
+ */
+private fun ComposeTestRule.tabThumbnail() = onNodeWithTag(TabsTrayTestTag.tabItemThumbnail)
+
+/**
+ * Obtains the three dot button in the Tabs Tray banner.
+ */
+private fun ComposeTestRule.threeDotButton() = onNodeWithTag(TabsTrayTestTag.threeDotButton)
+
+/**
+ * Obtains the dropdown menu item to access account settings.
+ */
+private fun ComposeTestRule.dropdownMenuItemAccountSettings() = onNodeWithTag(TabsTrayTestTag.accountSettings)
+
+/**
+ * Obtains the dropdown menu item to close all tabs.
+ */
+private fun ComposeTestRule.dropdownMenuItemCloseAllTabs() = onNodeWithTag(TabsTrayTestTag.closeAllTabs)
+
+/**
+ * Obtains the dropdown menu item to access recently closed tabs.
+ */
+private fun ComposeTestRule.dropdownMenuItemRecentlyClosedTabs() = onNodeWithTag(TabsTrayTestTag.recentlyClosedTabs)
+
+/**
+ * Obtains the dropdown menu item to select tabs.
+ */
+private fun ComposeTestRule.dropdownMenuItemSelectTabs() = onNodeWithTag(TabsTrayTestTag.selectTabs)
+
+/**
+ * Obtains the dropdown menu item to share all tabs.
+ */
+private fun ComposeTestRule.dropdownMenuItemShareAllTabs() = onNodeWithTag(TabsTrayTestTag.shareAllTabs)
+
+/**
+ * Obtains the dropdown menu item to access tab settings.
+ */
+private fun ComposeTestRule.dropdownMenuItemTabSettings() = onNodeWithTag(TabsTrayTestTag.tabSettings)
+
+/**
+ * Obtains the normal tabs counter.
+ */
+private fun ComposeTestRule.normalTabsCounter() = onNodeWithTag(TabsTrayTestTag.normalTabsCounter)
+
+/**
+ * Obtains the Tabs Tray banner collections button.
+ */
+private fun ComposeTestRule.collectionsButton() = onNodeWithTag(TabsTrayTestTag.collectionsButton)
+
+/**
+ * Obtains the Tabs Tray banner multi selection counter.
+ */
+private fun ComposeTestRule.multiSelectionCounter() = onNodeWithTag(TabsTrayTestTag.selectionCounter)
+
+/**
+ * Obtains the Tabs Tray banner handle.
+ */
+private fun ComposeTestRule.bannerHandle() = onNodeWithTag(TabsTrayTestTag.bannerHandle)
+
+/**
+ * Obtains the media control button with the given [action] as its content description.
+ */
+private fun ComposeTestRule.tabMediaControlButton(action: String) = onNodeWithContentDescription(action)
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTopSitesRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTopSitesRobot.kt
new file mode 100644
index 0000000000..53f1121bfa
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTopSitesRobot.kt
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.filter
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
+import androidx.compose.ui.test.performTouchInput
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.home.topsites.TopSitesTestTag
+
+/**
+ * Implementation of Robot Pattern for the Compose Top Sites.
+ */
+class ComposeTopSitesRobot(private val composeTestRule: HomeActivityComposeTestRule) {
+
+ @OptIn(ExperimentalTestApi::class)
+ fun verifyExistingTopSitesList() {
+ Log.i(TAG, "verifyExistingTopSitesList: Waiting for $waitingTime ms until the top sites list exists")
+ composeTestRule.waitUntilAtLeastOneExists(hasTestTag(TopSitesTestTag.topSites), timeoutMillis = waitingTime)
+ Log.i(TAG, "verifyExistingTopSitesList: Waited for $waitingTime ms until the top sites list to exists")
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ fun verifyExistingTopSiteItem(vararg titles: String) {
+ titles.forEach { title ->
+ Log.i(TAG, "verifyExistingTopSiteItem: Waiting for $waitingTime ms until the top site with title: $title exists")
+ composeTestRule.waitUntilAtLeastOneExists(hasText(title), timeoutMillis = waitingTime)
+ Log.i(TAG, "verifyExistingTopSiteItem: Waited for $waitingTime ms until the top site with title: $title exists")
+ Log.i(TAG, "verifyExistingTopSiteItem: Trying to verify that the top site with title: $title exists")
+ composeTestRule.topSiteItem(title).assertExists()
+ Log.i(TAG, "verifyExistingTopSiteItem: Verified that the top site with title: $title exists")
+ }
+ }
+
+ fun verifyNotExistingTopSiteItem(vararg titles: String) {
+ titles.forEach { title ->
+ Log.i(TAG, "verifyNotExistingTopSiteItem: Waiting for $waitingTime ms for top site with title: $title to exist")
+ itemContainingText(title).waitForExists(waitingTime)
+ Log.i(TAG, "verifyNotExistingTopSiteItem: Waited for $waitingTime ms for top site with title: $title to exist")
+ Log.i(TAG, "verifyNotExistingTopSiteItem: Trying to verify that top site with title: $title does not exist")
+ composeTestRule.topSiteItem(title).assertDoesNotExist()
+ Log.i(TAG, "verifyNotExistingTopSiteItem: Verified that top site with title: $title does not exist")
+ }
+ }
+
+ fun verifyTopSiteContextMenuItems() {
+ verifyTopSiteContextMenuOpenInPrivateTabButton()
+ verifyTopSiteContextMenuRemoveButton()
+ verifyTopSiteContextMenuRenameButton()
+ }
+
+ fun verifyTopSiteContextMenuOpenInPrivateTabButton() {
+ Log.i(TAG, "verifyTopSiteContextMenuOpenInPrivateTabButton: Trying to verify that the \"Open in private tab\" menu button exists")
+ composeTestRule.contextMenuItemOpenInPrivateTab().assertExists()
+ Log.i(TAG, "verifyTopSiteContextMenuOpenInPrivateTabButton: Verified that the \"Open in private tab\" menu button exists")
+ }
+
+ fun verifyTopSiteContextMenuRenameButton() {
+ Log.i(TAG, "verifyTopSiteContextMenuRenameButton: Trying to verify that the \"Rename\" menu button exists")
+ composeTestRule.contextMenuItemRename().assertExists()
+ Log.i(TAG, "verifyTopSiteContextMenuRenameButton: Verified that the \"Rename\" menu button exists")
+ }
+
+ fun verifyTopSiteContextMenuRemoveButton() {
+ Log.i(TAG, "verifyTopSiteContextMenuRemoveButton: Trying to verify that the \"Remove\" menu button exists")
+ composeTestRule.contextMenuItemRemove().assertExists()
+ Log.i(TAG, "verifyTopSiteContextMenuRemoveButton: Verified that the \"Remove\" menu button exists")
+ }
+
+ class Transition(private val composeTestRule: HomeActivityComposeTestRule) {
+
+ fun openTopSiteTabWithTitle(
+ title: String,
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ Log.i(TAG, "openTopSiteTabWithTitle: Trying to scroll to top site with title: $title")
+ composeTestRule.topSiteItem(title).performScrollTo()
+ Log.i(TAG, "openTopSiteTabWithTitle: Scrolled to top site with title: $title")
+ Log.i(TAG, "openTopSiteTabWithTitle: Trying to click top site with title: $title")
+ composeTestRule.topSiteItem(title).performClick()
+ Log.i(TAG, "openTopSiteTabWithTitle: Clicked top site with title: $title")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openTopSiteInPrivate(
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ Log.i(TAG, "openTopSiteInPrivate: Trying to click the \"Open in private tab\" menu button")
+ composeTestRule.contextMenuItemOpenInPrivateTab().performClick()
+ Log.i(TAG, "openTopSiteInPrivate: Clicked the \"Open in private tab\" menu button")
+ composeTestRule.waitForIdle()
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openContextMenuOnTopSitesWithTitle(
+ title: String,
+ interact: ComposeTopSitesRobot.() -> Unit,
+ ): Transition {
+ Log.i(TAG, "openContextMenuOnTopSitesWithTitle: Trying to scroll to top site with title: $title")
+ composeTestRule.topSiteItem(title).performScrollTo()
+ Log.i(TAG, "openContextMenuOnTopSitesWithTitle: Scrolled to top site with title: $title")
+ Log.i(TAG, "openContextMenuOnTopSitesWithTitle: Trying to long click top site with title: $title")
+ composeTestRule.topSiteItem(title).performTouchInput { longClick() }
+ Log.i(TAG, "openContextMenuOnTopSitesWithTitle: Long clicked top site with title: $title")
+
+ ComposeTopSitesRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ fun renameTopSite(
+ title: String,
+ interact: ComposeTopSitesRobot.() -> Unit,
+ ): Transition {
+ Log.i(TAG, "renameTopSite: Trying to click the \"Rename\" menu button")
+ composeTestRule.contextMenuItemRename().performClick()
+ Log.i(TAG, "renameTopSite: Clicked the \"Rename\" menu button")
+ itemWithResId("$packageName:id/top_site_title")
+ .also {
+ Log.i(TAG, "renameTopSite: Waiting for $waitingTimeShort ms for top site rename text box to exist")
+ it.waitForExists(waitingTimeShort)
+ Log.i(TAG, "renameTopSite: Waited for $waitingTimeShort ms for top site rename text box to exist")
+ Log.i(TAG, "renameTopSite: Trying to set top site rename text box text to: $title")
+ it.setText(title)
+ Log.i(TAG, "renameTopSite: Top site rename text box text was set to: $title")
+ }
+ Log.i(TAG, "renameTopSite: Trying to click the \"Ok\" dialog button")
+ itemWithResIdContainingText("android:id/button1", "OK").click()
+ Log.i(TAG, "renameTopSite: Clicked the \"Ok\" dialog button")
+
+ ComposeTopSitesRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ fun removeTopSite(
+ interact: ComposeTopSitesRobot.() -> Unit,
+ ): Transition {
+ Log.i(TAG, "removeTopSite: Trying to click the \"Remove\" menu button")
+ composeTestRule.contextMenuItemRemove().performClick()
+ Log.i(TAG, "removeTopSite: Clicked the \"Remove\" menu button")
+ Log.i(TAG, "removeTopSite: Waiting for $waitingTime ms until the \"Remove\" menu button does not exist")
+ composeTestRule.waitUntilDoesNotExist(hasTestTag(TopSitesTestTag.remove), waitingTime)
+ Log.i(TAG, "removeTopSite: Waited for $waitingTime ms until the \"Remove\" menu button does not exist")
+
+ ComposeTopSitesRobot(composeTestRule).interact()
+ return Transition(composeTestRule)
+ }
+ }
+}
+
+/**
+ * Obtains the top site with the provided [title].
+ */
+private fun ComposeTestRule.topSiteItem(title: String) =
+ onAllNodesWithTag(TopSitesTestTag.topSiteItemRoot).filter(hasAnyChild(hasText(title))).onFirst()
+
+/**
+ * Obtains the option to open in private tab the top site
+ */
+private fun ComposeTestRule.contextMenuItemOpenInPrivateTab() =
+ onAllNodesWithTag(TopSitesTestTag.openInPrivateTab).onFirst()
+
+/**
+ * Obtains the option to rename the top site
+ */
+private fun ComposeTestRule.contextMenuItemRename() = onAllNodesWithTag(TopSitesTestTag.rename).onFirst()
+
+/**
+ * Obtains the option to remove the top site
+ */
+private fun ComposeTestRule.contextMenuItemRemove() = onAllNodesWithTag(TopSitesTestTag.remove).onFirst()
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CustomTabRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CustomTabRobot.kt
new file mode 100644
index 0000000000..4a6e431b74
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CustomTabRobot.kt
@@ -0,0 +1,210 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.waitForObjects
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of the robot pattern for Custom tabs
+ */
+class CustomTabRobot {
+
+ fun verifyCustomTabsSiteInfoButton() =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/mozac_browser_toolbar_security_indicator"),
+ )
+
+ fun verifyCustomTabsShareButton() =
+ assertUIObjectExists(
+ itemWithDescription(getStringResource(R.string.mozac_feature_customtabs_share_link)),
+ )
+
+ fun verifyMainMenuButton() = assertUIObjectExists(mainMenuButton())
+
+ fun verifyDesktopSiteButtonExists() {
+ Log.i(TAG, "verifyDesktopSiteButtonExists: Trying to verify that the request desktop site button is displayed")
+ desktopSiteButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDesktopSiteButtonExists: Verified that the request desktop site button is displayed")
+ }
+
+ fun verifyFindInPageButtonExists() {
+ Log.i(TAG, "verifyFindInPageButtonExists: Trying to verify that the find in page button is displayed")
+ findInPageButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyFindInPageButtonExists: Verified that the find in page button is displayed")
+ }
+
+ fun verifyPoweredByTextIsDisplayed() =
+ assertUIObjectExists(itemContainingText("POWERED BY $appName"))
+
+ fun verifyOpenInBrowserButtonExists() {
+ Log.i(TAG, "verifyOpenInBrowserButtonExists: Trying to verify that the \"Open in Firefox\" button is displayed")
+ openInBrowserButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyOpenInBrowserButtonExists: Verified that the \"Open in Firefox\" button is displayed")
+ }
+
+ fun verifyBackButtonExists() = assertUIObjectExists(itemWithDescription("Back"))
+
+ fun verifyForwardButtonExists() = assertUIObjectExists(itemWithDescription("Forward"))
+
+ fun verifyRefreshButtonExists() = assertUIObjectExists(itemWithDescription("Refresh"))
+
+ fun verifyCustomMenuItem(label: String) = assertUIObjectExists(itemContainingText(label))
+
+ fun verifyCustomTabCloseButton() {
+ Log.i(TAG, "verifyCustomTabCloseButton: Trying to verify that the close custom tab button is displayed")
+ closeButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTabCloseButton: Verified that the close custom tab button is displayed")
+ }
+
+ fun verifyCustomTabToolbarTitle(title: String) {
+ waitForPageToLoad()
+
+ mDevice.waitForObjects(
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/mozac_browser_toolbar_title_view")
+ .textContains(title),
+ )
+ .getFromParent(
+ UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_origin_view"),
+ ),
+ waitingTime,
+ )
+
+ assertUIObjectExists(
+ itemWithResIdContainingText("$packageName:id/mozac_browser_toolbar_title_view", title),
+ )
+ }
+
+ fun verifyCustomTabUrl(Url: String) {
+ assertUIObjectExists(
+ itemWithResIdContainingText("$packageName:id/mozac_browser_toolbar_url_view", Url.drop(7)),
+ )
+ }
+
+ fun longCLickAndCopyToolbarUrl() {
+ mDevice.waitForObjects(
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")),
+ waitingTime,
+ )
+ Log.i(TAG, "longCLickAndCopyToolbarUrl: Trying to long click the custom tab toolbar")
+ customTabToolbar().click(LONG_CLICK_DURATION)
+ Log.i(TAG, "longCLickAndCopyToolbarUrl: Long clicked the custom tab toolbar")
+ clickContextMenuItem("Copy")
+ }
+
+ fun fillAndSubmitLoginCredentials(userName: String, password: String) {
+ Log.i(TAG, "fillAndSubmitLoginCredentials: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "fillAndSubmitLoginCredentials: Waited for device to be idle for $waitingTime ms")
+ setPageObjectText(itemWithResId("username"), userName)
+ setPageObjectText(itemWithResId("password"), password)
+ clickPageObject(itemWithResId("submit"))
+ mDevice.waitForObjects(
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/save_confirm")),
+ waitingTime,
+ )
+ }
+
+ fun waitForPageToLoad() {
+ Log.i(TAG, "waitForPageToLoad: Waiting for $waitingTime ms until progress bar is gone")
+ progressBar().waitUntilGone(waitingTime)
+ Log.i(TAG, "waitForPageToLoad: Waited for $waitingTime ms until progress bar was gone")
+ }
+
+ fun clickCustomTabCloseButton() {
+ Log.i(TAG, "clickCustomTabCloseButton: Trying to click close custom tab button")
+ closeButton().click()
+ Log.i(TAG, "clickCustomTabCloseButton: Clicked close custom tab button")
+ }
+
+ fun verifyCustomTabActionButton(customTabActionButtonDescription: String) =
+ assertUIObjectExists(itemWithDescription(customTabActionButtonDescription))
+
+ fun verifyPDFReaderToolbarItems() =
+ assertUIObjectExists(
+ itemWithResIdAndText("download", "Download"),
+ )
+
+ class Transition {
+ fun openMainMenu(interact: CustomTabRobot.() -> Unit): Transition {
+ mainMenuButton().also {
+ Log.i(TAG, "openMainMenu: Waiting for $waitingTime ms for the main menu button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "openMainMenu: Waited for $waitingTime ms for the main menu button to exist")
+ Log.i(TAG, "openMainMenu: Trying to click the main menu button")
+ it.click()
+ Log.i(TAG, "openMainMenu: Clicked the main menu button")
+ }
+
+ CustomTabRobot().interact()
+ return Transition()
+ }
+
+ fun clickOpenInBrowserButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickOpenInBrowserButton: Trying to click the \"Open in Firefox\" button")
+ openInBrowserButton().perform(click())
+ Log.i(TAG, "clickOpenInBrowserButton: Clicked the \"Open in Firefox\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ Log.i(TAG, "clickShareButton: Trying to click the share button")
+ itemWithDescription(getStringResource(R.string.mozac_feature_customtabs_share_link)).click()
+ Log.i(TAG, "clickShareButton: Clicked the share button")
+
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+ }
+ }
+}
+
+fun customTabScreen(interact: CustomTabRobot.() -> Unit): CustomTabRobot.Transition {
+ CustomTabRobot().interact()
+ return CustomTabRobot.Transition()
+}
+
+private fun mainMenuButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_menu")
+
+private fun desktopSiteButton() = onView(withId(R.id.switch_widget))
+
+private fun findInPageButton() = onView(withText("Find in page"))
+
+private fun openInBrowserButton() = onView(withText("Open in $appName"))
+
+private fun closeButton() = onView(withContentDescription("Return to previous app"))
+
+private fun customTabToolbar() = mDevice.findObject(By.res("$packageName:id/toolbar"))
+
+private fun progressBar() =
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_progress"),
+ )
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DeepLinkRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DeepLinkRobot.kt
new file mode 100644
index 0000000000..db4cb29031
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DeepLinkRobot.kt
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import androidx.test.platform.app.InstrumentationRegistry
+import org.mozilla.fenix.BuildConfig.DEEP_LINK_SCHEME
+
+class DeepLinkRobot {
+ private fun openDeepLink(url: String) {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val intent = Intent().apply {
+ action = Intent.ACTION_VIEW
+ data = Uri.parse("$DEEP_LINK_SCHEME://$url")
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ addCategory(Intent.CATEGORY_BROWSABLE)
+ }
+ try {
+ context.startActivity(intent)
+ } catch (ex: ActivityNotFoundException) {
+ intent.setPackage(null)
+ context.startActivity(intent)
+ }
+ }
+
+ fun openURL(url: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ val deepLink = Uri.parse("open")
+ .buildUpon()
+ .appendQueryParameter("url", url)
+ .build()
+ .toString()
+ openDeepLink(deepLink)
+ return browserScreen(interact)
+ }
+
+ fun openHomeScreen(interact: HomeScreenRobot.() -> Unit) =
+ openDeepLink("home").run { homeScreen(interact) }
+
+ fun openBookmarks(interact: BookmarksRobot.() -> Unit) =
+ openDeepLink("urls_bookmarks").run { bookmarksMenu(interact) }
+
+ fun openHistory(interact: HistoryRobot.() -> Unit) =
+ openDeepLink("urls_history").run { historyMenu(interact) }
+
+ fun openCollections(interact: HomeScreenRobot.() -> Unit) =
+ openDeepLink("home_collections").run { homeScreen(interact) }
+
+ fun openSettings(interact: SettingsRobot.() -> Unit) =
+ openDeepLink("settings").run { settings(interact) }
+
+ fun openSettingsPrivacy(interact: SettingsRobot.() -> Unit) =
+ openDeepLink("settings_privacy").run { settings(interact) }
+
+ fun openSettingsLogins(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit) =
+ openDeepLink("settings_logins").run { settingsSubMenuLoginsAndPassword(interact) }
+
+ fun openSettingsTrackingProtection(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit) =
+ openDeepLink("settings_tracking_protection").run {
+ settingsSubMenuEnhancedTrackingProtection(interact)
+ }
+
+ fun openSettingsSearchEngine(interact: SettingsSubMenuSearchRobot.() -> Unit) =
+ openDeepLink("settings_search_engine").run {
+ SettingsSubMenuSearchRobot().interact()
+ SettingsSubMenuSearchRobot.Transition()
+ }
+
+ fun openSettingsNotification(interact: SystemSettingsRobot.() -> Unit) =
+ openDeepLink("settings_notifications").run { systemSettings(interact) }
+
+ fun openMakeDefaultBrowser(interact: SystemSettingsRobot.() -> Unit) =
+ openDeepLink("make_default_browser").run { systemSettings(interact) }
+}
+
+private fun settings(interact: SettingsRobot.() -> Unit) =
+ SettingsRobot().interact().run { SettingsRobot.Transition() }
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt
new file mode 100644
index 0000000000..eab72fffb1
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt
@@ -0,0 +1,277 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
+import org.mozilla.fenix.helpers.AppAndSystemHelper.getPermissionAllowID
+import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+/**
+ * Implementation of Robot Pattern for download UI handling.
+ */
+
+class DownloadRobot {
+
+ fun verifyDownloadPrompt(fileName: String) {
+ var currentTries = 0
+ while (currentTries++ < 3) {
+ Log.i(TAG, "verifyDownloadPrompt: Started try #$currentTries")
+ try {
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/download_button"),
+ itemContainingText(fileName),
+ )
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyDownloadPrompt: AssertionError caught, executing fallback methods")
+ Log.e("DOWNLOAD_ROBOT", "Failed to find locator: ${e.localizedMessage}")
+
+ browserScreen {
+ }.clickDownloadLink(fileName) {
+ }
+ }
+ }
+ }
+
+ fun verifyDownloadCompleteNotificationPopup() =
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.mozac_feature_downloads_button_open)),
+ itemContainingText(getStringResource(R.string.mozac_feature_downloads_completed_notification_text2)),
+ itemWithResId("$packageName:id/download_dialog_filename"),
+ )
+
+ fun verifyDownloadFailedPrompt(fileName: String) =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/download_dialog_icon"),
+ itemWithResIdContainingText(
+ "$packageName:id/download_dialog_title",
+ getStringResource(R.string.mozac_feature_downloads_failed_notification_text2),
+ ),
+ itemWithResIdContainingText(
+ "$packageName:id/download_dialog_filename",
+ fileName,
+ ),
+ itemWithResIdContainingText(
+ "$packageName:id/download_dialog_action_button",
+ getStringResource(R.string.mozac_feature_downloads_button_try_again),
+ ),
+ )
+
+ fun clickTryAgainButton() {
+ Log.i(TAG, "clickTryAgainButton: Trying to click the \"TRY AGAIN\" in app prompt button")
+ itemWithResIdAndText(
+ "$packageName:id/download_dialog_action_button",
+ "Try Again",
+ ).click()
+ Log.i(TAG, "clickTryAgainButton: Clicked the \"TRY AGAIN\" in app prompt button")
+ }
+
+ fun verifyPhotosAppOpens() = assertExternalAppOpens(GOOGLE_APPS_PHOTOS)
+
+ fun verifyDownloadedFileName(fileName: String) =
+ assertUIObjectExists(itemContainingText(fileName))
+
+ fun verifyDownloadedFileIcon() = assertUIObjectExists(itemWithResId("$packageName:id/favicon"))
+
+ fun verifyEmptyDownloadsList() {
+ Log.i(TAG, "verifyEmptyDownloadsList: Waiting for $waitingTime ms for for empty download list to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/download_empty_view"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "verifyEmptyDownloadsList: Waited for $waitingTime ms for for empty download list to exist")
+ Log.i(TAG, "verifyEmptyDownloadsList: Trying to verify that the \"No downloaded files\" list message is displayed")
+ onView(withText("No downloaded files")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyEmptyDownloadsList: Verified that the \"No downloaded files\" list message is displayed")
+ }
+
+ fun waitForDownloadsListToExist() =
+ assertUIObjectExists(itemWithResId("$packageName:id/download_list"))
+
+ fun openDownloadedFile(fileName: String) {
+ Log.i(TAG, "openDownloadedFile: Trying to verify that the downloaded file: $fileName is displayed")
+ downloadedFile(fileName).check(matches(isDisplayed()))
+ Log.i(TAG, "openDownloadedFile: Verified that the downloaded file: $fileName is displayed")
+ Log.i(TAG, "openDownloadedFile: Trying to click downloaded file: $fileName")
+ downloadedFile(fileName).click()
+ Log.i(TAG, "openDownloadedFile: Clicked downloaded file: $fileName")
+ }
+
+ fun deleteDownloadedItem(fileName: String) {
+ Log.i(TAG, "deleteDownloadedItem: Trying to click the trash bin icon to delete downloaded file: $fileName")
+ onView(
+ allOf(
+ withId(R.id.overflow_menu),
+ hasSibling(withText(fileName)),
+ ),
+ ).click()
+ Log.i(TAG, "deleteDownloadedItem: Clicked the trash bin icon to delete downloaded file: $fileName")
+ }
+
+ fun longClickDownloadedItem(title: String) {
+ Log.i(TAG, "longClickDownloadedItem: Trying to long click downloaded file: $title")
+ onView(
+ allOf(
+ withId(R.id.title),
+ withText(title),
+ ),
+ ).perform(longClick())
+ Log.i(TAG, "longClickDownloadedItem: Long clicked downloaded file: $title")
+ }
+
+ fun selectDownloadedItem(title: String) {
+ Log.i(TAG, "selectDownloadedItem: Trying click downloaded file: $title to select it")
+ onView(
+ allOf(
+ withId(R.id.title),
+ withText(title),
+ ),
+ ).perform(click())
+ Log.i(TAG, "selectDownloadedItem: Clicked downloaded file: $title to select it")
+ }
+
+ fun openMultiSelectMoreOptionsMenu() {
+ Log.i(TAG, "openMultiSelectMoreOptionsMenu: Trying to click multi-select more options button")
+ itemWithDescription(getStringResource(R.string.content_description_menu)).click()
+ Log.i(TAG, "openMultiSelectMoreOptionsMenu: Clicked multi-select more options button")
+ }
+
+ fun clickMultiSelectRemoveButton() {
+ Log.i(TAG, "clickMultiSelectRemoveButton: Trying to click multi-select remove button")
+ itemWithResIdContainingText("$packageName:id/title", "Remove").click()
+ Log.i(TAG, "clickMultiSelectRemoveButton: Clicked multi-select remove button")
+ }
+
+ fun openPageAndDownloadFile(url: Uri, downloadFile: String) {
+ navigationToolbar {
+ }.enterURLAndEnterToBrowser(url) {
+ waitForPageToLoad()
+ }.clickDownloadLink(downloadFile) {
+ verifyDownloadPrompt(downloadFile)
+ }.clickDownload {
+ }
+ }
+
+ class Transition {
+ fun clickDownload(interact: DownloadRobot.() -> Unit): Transition {
+ Log.i(TAG, "clickDownload: Trying to click the \"Download\" download prompt button")
+ downloadButton().click()
+ Log.i(TAG, "clickDownload: Clicked the \"Download\" download prompt button")
+
+ DownloadRobot().interact()
+ return Transition()
+ }
+
+ fun closeDownloadPrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeDownloadPrompt: Trying to click the close download prompt button")
+ itemWithResId("$packageName:id/download_dialog_close_button").click()
+ Log.i(TAG, "closeDownloadPrompt: Clicked the close download prompt button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickOpen(type: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickOpen: Waiting for $waitingTime ms for the for \"OPEN\" download prompt button to exist")
+ openDownloadButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickOpen: Waited for $waitingTime ms for the for \"OPEN\" download prompt button to exist")
+ Log.i(TAG, "clickOpen: Trying to click the \"OPEN\" download prompt button")
+ openDownloadButton().click()
+ Log.i(TAG, "clickOpen: Clicked the \"OPEN\" download prompt button")
+ Log.i(TAG, "clickOpen: Trying to verify that the open intent is matched with associated data type")
+ // verify open intent is matched with associated data type
+ Intents.intended(
+ allOf(
+ IntentMatchers.hasAction(Intent.ACTION_VIEW),
+ IntentMatchers.hasType(type),
+ ),
+ )
+ Log.i(TAG, "clickOpen: Verified that the open intent is matched with associated data type")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickAllowPermission(interact: DownloadRobot.() -> Unit): Transition {
+ mDevice.waitNotNull(
+ Until.findObject(By.res(getPermissionAllowID() + ":id/permission_allow_button")),
+ waitingTime,
+ )
+ Log.i(TAG, "clickAllowPermission: Trying to click the \"ALLOW\" permission button")
+ mDevice.findObject(By.res(getPermissionAllowID() + ":id/permission_allow_button")).click()
+ Log.i(TAG, "clickAllowPermission: Clicked the \"ALLOW\" permission button")
+
+ DownloadRobot().interact()
+ return Transition()
+ }
+
+ fun exitDownloadsManagerToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "exitDownloadsManagerToBrowser: Trying to click the navigate up toolbar button")
+ onView(withContentDescription("Navigate up")).click()
+ Log.i(TAG, "exitDownloadsManagerToBrowser: Clicked the navigate up toolbar button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+ }
+}
+
+fun downloadRobot(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
+ DownloadRobot().interact()
+ return DownloadRobot.Transition()
+}
+
+private fun downloadButton() =
+ onView(withId(R.id.download_button))
+ .check(matches(isDisplayed()))
+
+private fun openDownloadButton() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/download_dialog_action_button"))
+
+private fun downloadedFile(fileName: String) = onView(withText(fileName))
+
+private fun goBackButton() = onView(withContentDescription("Navigate up"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt
new file mode 100644
index 0000000000..365b30c982
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt
@@ -0,0 +1,344 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.containsString
+import org.hamcrest.Matchers.not
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.helpers.isChecked
+
+/**
+ * Implementation of Robot Pattern for Enhanced Tracking Protection UI.
+ */
+class EnhancedTrackingProtectionRobot {
+ fun verifyEnhancedTrackingProtectionSheetStatus(status: String, state: Boolean) {
+ mDevice.waitNotNull(Until.findObjects(By.text("Protections are $status for this site")))
+ Log.i(TAG, "verifyEnhancedTrackingProtectionSheetStatus: Trying to check ETP toggle is checked: $state")
+ onView(ViewMatchers.withResourceName("switch_widget")).check(
+ matches(
+ isChecked(
+ state,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyEnhancedTrackingProtectionSheetStatus: Verified ETP toggle is checked: $state")
+ }
+
+ fun verifyETPSwitchVisibility(visible: Boolean) {
+ if (visible) {
+ Log.i(TAG, "verifyETPSwitchVisibility: Trying to verify ETP toggle is displayed")
+ enhancedTrackingProtectionSwitch()
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyETPSwitchVisibility: Verified ETP toggle is displayed")
+ } else {
+ Log.i(TAG, "verifyETPSwitchVisibility: Trying to verify ETP toggle is not displayed")
+ enhancedTrackingProtectionSwitch()
+ .check(matches(not(isDisplayed())))
+ Log.i(TAG, "verifyETPSwitchVisibility: Verified ETP toggle is not displayed")
+ }
+ }
+
+ fun verifyCrossSiteCookiesBlocked(isBlocked: Boolean) {
+ assertUIObjectExists(itemWithResId("$packageName:id/cross_site_tracking"))
+ Log.i(TAG, "verifyCrossSiteCookiesBlocked: Trying to click cross site cookies block list button")
+ crossSiteCookiesBlockListButton().click()
+ Log.i(TAG, "verifyCrossSiteCookiesBlocked: Clicked cross site cookies block list button")
+ // Verifies the trackers block/allow list
+ Log.i(TAG, "verifyCrossSiteCookiesBlocked: Trying to verify cross site cookies are blocked: $isBlocked")
+ onView(withId(R.id.details_blocking_header))
+ .check(
+ matches(
+ withText(
+ if (isBlocked) {
+ ("Blocked")
+ } else {
+ ("Allowed")
+ },
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyCrossSiteCookiesBlocked: Verified cross site cookies are blocked: $isBlocked")
+ }
+
+ fun verifySocialMediaTrackersBlocked(isBlocked: Boolean) {
+ assertUIObjectExists(itemWithResId("$packageName:id/social_media_trackers"))
+ Log.i(TAG, "verifySocialMediaTrackersBlocked: Trying to click social trackers block list button")
+ socialTrackersBlockListButton().click()
+ Log.i(TAG, "verifySocialMediaTrackersBlocked: Clicked social trackers block list button")
+ // Verifies the trackers block/allow list
+ Log.i(TAG, "verifySocialMediaTrackersBlocked: Trying to verify social trackers are blocked: $isBlocked")
+ onView(withId(R.id.details_blocking_header))
+ .check(
+ matches(
+ withText(
+ if (isBlocked) {
+ ("Blocked")
+ } else {
+ ("Allowed")
+ },
+ ),
+ ),
+ )
+ Log.i(TAG, "verifySocialMediaTrackersBlocked: Verified social trackers are blocked: $isBlocked")
+ Log.i(TAG, "verifySocialMediaTrackersBlocked: Trying to verify blocked social trackers list is displayed")
+ onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifySocialMediaTrackersBlocked: Verified blocked social trackers list is displayed")
+ }
+
+ fun verifyFingerprintersBlocked(isBlocked: Boolean) {
+ assertUIObjectExists(itemWithResId("$packageName:id/fingerprinters"))
+ Log.i(TAG, "verifyFingerprintersBlocked: Trying to click fingerprinters block list button")
+ fingerprintersBlockListButton().click()
+ Log.i(TAG, "verifyFingerprintersBlocked: Clicked fingerprinters block list button")
+ // Verifies the trackers block/allow list
+ Log.i(TAG, "verifyFingerprintersBlocked: Trying to verify fingerprinters are blocked: $isBlocked")
+ onView(withId(R.id.details_blocking_header))
+ .check(
+ matches(
+ withText(
+ if (isBlocked) {
+ ("Blocked")
+ } else {
+ ("Allowed")
+ },
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyFingerprintersBlocked: Verified fingerprinters are blocked: $isBlocked")
+ Log.i(TAG, "verifyFingerprintersBlocked: Trying to verify blocked fingerprinter trackers list is displayed")
+ onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyFingerprintersBlocked: Verified blocked fingerprinter trackers list is displayed")
+ }
+
+ fun verifyCryptominersBlocked(isBlocked: Boolean) {
+ assertUIObjectExists(itemWithResId("$packageName:id/cryptominers"))
+ Log.i(TAG, "verifyCryptominersBlocked: Trying to click cryptominers block list button")
+ cryptominersBlockListButton().click()
+ Log.i(TAG, "verifyCryptominersBlocked: Clicked cryptominers block list button")
+ // Verifies the trackers block/allow list
+ Log.i(TAG, "verifyCryptominersBlocked: Trying to verify cryptominers are blocked: $isBlocked")
+ onView(withId(R.id.details_blocking_header))
+ .check(
+ matches(
+ withText(
+ if (isBlocked) {
+ ("Blocked")
+ } else {
+ ("Allowed")
+ },
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyCryptominersBlocked: Verified cryptominers are blocked: $isBlocked")
+ Log.i(TAG, "verifyCryptominersBlocked: Trying to verify blocked cryptominers trackers list is displayed")
+ onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCryptominersBlocked: Verified blocked cryptominers trackers list is displayed")
+ }
+
+ fun verifyTrackingContentBlocked(isBlocked: Boolean) {
+ assertUIObjectExists(itemWithText("Tracking Content"))
+ Log.i(TAG, "verifyTrackingContentBlocked: Trying to click tracking content block list button")
+ trackingContentBlockListButton().click()
+ Log.i(TAG, "verifyTrackingContentBlocked: Clicked tracking content block list button")
+ // Verifies the trackers block/allow list
+ Log.i(TAG, "verifyTrackingContentBlocked: Trying to verify tracking content is blocked: $isBlocked")
+ onView(withId(R.id.details_blocking_header))
+ .check(
+ matches(
+ withText(
+ if (isBlocked) {
+ ("Blocked")
+ } else {
+ ("Allowed")
+ },
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyTrackingContentBlocked: Verified tracking content is blocked: $isBlocked")
+ Log.i(TAG, "verifyTrackingContentBlocked: Trying to verify blocked tracking content trackers list is displayed")
+ onView(withId(R.id.blocking_text_list)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyTrackingContentBlocked: Verified blocked tracking content trackers list is displayed")
+ }
+
+ fun viewTrackingContentBlockList() {
+ Log.i(TAG, "viewTrackingContentBlockList: Trying to verify blocked tracking content trackers")
+ onView(withId(R.id.blocking_text_list))
+ .check(
+ matches(
+ withText(
+ containsString(
+ "social-track-digest256.dummytracker.org\n" +
+ "ads-track-digest256.dummytracker.org\n" +
+ "analytics-track-digest256.dummytracker.org",
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "viewTrackingContentBlockList: Verified blocked tracking content trackers")
+ }
+
+ fun verifyETPSectionIsDisplayedInQuickSettingsSheet(isDisplayed: Boolean) =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/trackingProtectionLayout"),
+ exists = isDisplayed,
+ )
+
+ fun navigateBackToDetails() {
+ Log.i(TAG, "navigateBackToDetails: Trying to click details list back button")
+ onView(withId(R.id.details_back)).click()
+ Log.i(TAG, "navigateBackToDetails: Clicked details list back button")
+ }
+
+ class Transition {
+ fun openEnhancedTrackingProtectionSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
+ Log.i(TAG, "openEnhancedTrackingProtectionSheet: Waiting for $waitingTime ms for site security button to exist")
+ pageSecurityIndicator().waitForExists(waitingTime)
+ Log.i(TAG, "openEnhancedTrackingProtectionSheet: Waited for $waitingTime ms for site security button to exist")
+ Log.i(TAG, "openEnhancedTrackingProtectionSheet: Trying to click site security button")
+ pageSecurityIndicator().click()
+ Log.i(TAG, "openEnhancedTrackingProtectionSheet: Clicked site security button")
+ Log.i(TAG, "openEnhancedTrackingProtectionSheet: Waiting for $waitingTime ms for quick actions sheet to exits")
+ mDevice.findObject(UiSelector().description(getStringResource(R.string.quick_settings_sheet)))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "openEnhancedTrackingProtectionSheet: Waited for $waitingTime ms for quick actions sheet to exits")
+ assertUIObjectExists(itemWithResId("$packageName:id/quick_action_sheet"))
+
+ EnhancedTrackingProtectionRobot().interact()
+ return Transition()
+ }
+
+ fun closeEnhancedTrackingProtectionSheet(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ // Back out of the Enhanced Tracking Protection sheet
+ Log.i(TAG, "closeEnhancedTrackingProtectionSheet: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "closeEnhancedTrackingProtectionSheet: Clicked device back button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun toggleEnhancedTrackingProtectionFromSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
+ Log.i(TAG, "toggleEnhancedTrackingProtectionFromSheet: Trying to click ETP switch")
+ enhancedTrackingProtectionSwitch().click()
+ Log.i(TAG, "toggleEnhancedTrackingProtectionFromSheet: Clicked ETP switch")
+
+ EnhancedTrackingProtectionRobot().interact()
+ return Transition()
+ }
+
+ fun openProtectionSettings(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): SettingsSubMenuEnhancedTrackingProtectionRobot.Transition {
+ Log.i(TAG, "openProtectionSettings: Waiting for $waitingTime ms for ETP sheet \"Details\" button to exist")
+ openEnhancedTrackingProtectionDetails().waitForExists(waitingTime)
+ Log.i(TAG, "openProtectionSettings: Waited for $waitingTime ms for ETP sheet \"Details\" button to exist")
+ Log.i(TAG, "openProtectionSettings: Trying to click ETP sheet \"Details\" button")
+ openEnhancedTrackingProtectionDetails().click()
+ Log.i(TAG, "openProtectionSettings: Clicked ETP sheet \"Details\" button")
+ Log.i(TAG, "openProtectionSettings: Trying to click \"Protection Settings\" button")
+ trackingProtectionSettingsButton().click()
+ Log.i(TAG, "openProtectionSettings: Clicked \"Protection Settings\" button")
+
+ SettingsSubMenuEnhancedTrackingProtectionRobot().interact()
+ return SettingsSubMenuEnhancedTrackingProtectionRobot.Transition()
+ }
+
+ fun openDetails(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
+ Log.i(TAG, "openDetails: Waiting for $waitingTime ms for ETP sheet \"Details\" button to exist")
+ openEnhancedTrackingProtectionDetails().waitForExists(waitingTime)
+ Log.i(TAG, "openDetails: Waited for $waitingTime ms for ETP sheet \"Details\" button to exist")
+ Log.i(TAG, "openDetails: Trying to click ETP sheet \"Details\" button")
+ openEnhancedTrackingProtectionDetails().click()
+ Log.i(TAG, "openDetails: Clicked ETP sheet \"Details\" button")
+
+ EnhancedTrackingProtectionRobot().interact()
+ return Transition()
+ }
+ }
+}
+
+fun enhancedTrackingProtection(interact: EnhancedTrackingProtectionRobot.() -> Unit): EnhancedTrackingProtectionRobot.Transition {
+ EnhancedTrackingProtectionRobot().interact()
+ return EnhancedTrackingProtectionRobot.Transition()
+}
+
+private fun pageSecurityIndicator() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_security_indicator"))
+
+private fun enhancedTrackingProtectionSwitch() =
+ onView(ViewMatchers.withResourceName("switch_widget"))
+
+private fun trackingProtectionSettingsButton() =
+ onView(withId(R.id.protection_settings)).inRoot(RootMatchers.isDialog()).check(
+ matches(
+ isDisplayed(),
+ ),
+ )
+
+private fun openEnhancedTrackingProtectionDetails() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/trackingProtectionDetails"))
+
+private fun trackingContentBlockListButton() =
+ onView(
+ allOf(
+ withText("Tracking Content"),
+ withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
+ ),
+ )
+
+private fun socialTrackersBlockListButton() =
+ onView(
+ allOf(
+ withId(R.id.social_media_trackers),
+ withText("Social Media Trackers"),
+ ),
+ )
+
+private fun crossSiteCookiesBlockListButton() =
+ onView(
+ allOf(
+ withId(R.id.cross_site_tracking),
+ withText("Cross-Site Cookies"),
+ ),
+ )
+
+private fun cryptominersBlockListButton() =
+ onView(
+ allOf(
+ withId(R.id.cryptominers),
+ withText("Cryptominers"),
+ ),
+ )
+
+private fun fingerprintersBlockListButton() =
+ onView(
+ allOf(
+ withId(R.id.fingerprinters),
+ withText("Fingerprinters"),
+ ),
+ )
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/FindInPageRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/FindInPageRobot.kt
new file mode 100644
index 0000000000..7f0789c227
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/FindInPageRobot.kt
@@ -0,0 +1,113 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.clearText
+import androidx.test.espresso.action.ViewActions.typeText
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+/**
+ * Implementation of Robot Pattern for the find in page UI.
+ */
+class FindInPageRobot {
+ fun verifyFindInPageNextButton() {
+ Log.i(TAG, "verifyFindInPageNextButton: Trying to verify find in page next result button is visible")
+ findInPageNextButton()
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyFindInPageNextButton: Verified find in page next result button is visible")
+ }
+ fun verifyFindInPagePrevButton() {
+ Log.i(TAG, "verifyFindInPagePrevButton: Trying to verify find in page previous result button is visible")
+ findInPagePrevButton()
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyFindInPagePrevButton: Verified find in page previous result button is visible")
+ }
+ fun verifyFindInPageCloseButton() {
+ Log.i(TAG, "verifyFindInPageCloseButton: Trying to verify find in page close button is visible")
+ findInPageCloseButton()
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyFindInPageCloseButton: Verified find in page close button is visible")
+ }
+ fun clickFindInPageNextButton() {
+ Log.i(TAG, "clickFindInPageNextButton: Trying to click next result button")
+ findInPageNextButton().click()
+ Log.i(TAG, "clickFindInPageNextButton: Clicked next result button")
+ }
+ fun clickFindInPagePrevButton() {
+ Log.i(TAG, "clickFindInPagePrevButton: Trying to click previous result button")
+ findInPagePrevButton().click()
+ Log.i(TAG, "clickFindInPagePrevButton: Clicked previous result button")
+ }
+
+ fun enterFindInPageQuery(expectedText: String) {
+ mDevice.waitNotNull(Until.findObject(By.res("org.mozilla.fenix.debug:id/find_in_page_query_text")), waitingTime)
+ Log.i(TAG, "enterFindInPageQuery: Trying to clear find in page bar text")
+ findInPageQuery().perform(clearText())
+ Log.i(TAG, "enterFindInPageQuery: Cleared find in page bar text")
+ mDevice.waitNotNull(Until.gone(By.res("org.mozilla.fenix.debug:id/find_in_page_result_text")), waitingTime)
+ Log.i(TAG, "enterFindInPageQuery: Trying to type $expectedText in find in page bar")
+ findInPageQuery().perform(typeText(expectedText))
+ Log.i(TAG, "enterFindInPageQuery: Typed $expectedText in find page bar")
+ mDevice.waitNotNull(Until.findObject(By.res("org.mozilla.fenix.debug:id/find_in_page_result_text")), waitingTime)
+ }
+
+ fun verifyFindInPageResult(ratioCounter: String) {
+ mDevice.waitNotNull(Until.findObject(By.text(ratioCounter)), waitingTime)
+ Log.i(TAG, "verifyFindInPageResult: Trying to verify $ratioCounter results")
+ findInPageResult().check(matches(withText((ratioCounter))))
+ Log.i(TAG, "verifyFindInPageResult: Verified $ratioCounter results")
+ }
+
+ class Transition {
+ fun closeFindInPageWithCloseButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeFindInPageWithCloseButton: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "closeFindInPageWithCloseButton: Device was idle")
+ Log.i(TAG, "closeFindInPageWithCloseButton: Trying to close find in page button")
+ findInPageCloseButton().click()
+ Log.i(TAG, "closeFindInPageWithCloseButton: Clicked close find in page button")
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun closeFindInPageWithBackButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeFindInPageWithBackButton: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "closeFindInPageWithBackButton: Device was idle")
+
+ // Will need to press back 2x, the first will only dismiss the keyboard
+ Log.i(TAG, "closeFindInPageWithBackButton: Trying to press 1x the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "closeFindInPageWithBackButton: Pressed 1x the device back button")
+ Log.i(TAG, "closeFindInPageWithBackButton: Trying to press 2x the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "closeFindInPageWithBackButton: Pressed 2x the device back button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+private fun findInPageQuery() = onView(withId(R.id.find_in_page_query_text))
+private fun findInPageResult() = onView(withId(R.id.find_in_page_result_text))
+private fun findInPageNextButton() = onView(withId(R.id.find_in_page_next_btn))
+private fun findInPagePrevButton() = onView(withId(R.id.find_in_page_prev_btn))
+private fun findInPageCloseButton() = onView(withId(R.id.find_in_page_close_btn))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt
new file mode 100644
index 0000000000..5e82ae4e04
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt
@@ -0,0 +1,240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.net.Uri
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.Matchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+/**
+ * Implementation of Robot Pattern for the history menu.
+ */
+class HistoryRobot {
+
+ fun verifyHistoryMenuView() {
+ Log.i(TAG, "verifyHistoryMenuView: Trying to verify that history menu view is visible")
+ onView(
+ allOf(withText("History"), withParent(withId(R.id.navigationToolbar))),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHistoryMenuView: Verified that history menu view is visible")
+ }
+
+ fun verifyEmptyHistoryView() {
+ Log.i(TAG, "verifyEmptyHistoryView: Waiting for $waitingTime ms for empty history list view to exist")
+ mDevice.findObject(
+ UiSelector().text("No history here"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifyEmptyHistoryView: Waited for $waitingTime ms for empty history list view to exist")
+
+ Log.i(TAG, "verifyEmptyHistoryView: Trying to verify empty history list view")
+ onView(
+ allOf(
+ withId(R.id.history_empty_view),
+ withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
+ ),
+ ).check(matches(withText("No history here")))
+ Log.i(TAG, "verifyEmptyHistoryView: Verified empty history list view")
+ }
+
+ fun verifyHistoryListExists() = assertUIObjectExists(itemWithResId("$packageName:id/history_list"))
+
+ fun verifyVisitedTimeTitle() {
+ mDevice.waitNotNull(
+ Until.findObject(
+ By.text("Today"),
+ ),
+ waitingTime,
+ )
+ Log.i(TAG, "verifyVisitedTimeTitle: Trying to verify \"Today\" chronological timeline title")
+ onView(withId(R.id.header_title)).check(matches(withText("Today")))
+ Log.i(TAG, "verifyVisitedTimeTitle: Verified \"Today\" chronological timeline title")
+ }
+
+ fun verifyHistoryItemExists(shouldExist: Boolean, item: String) =
+ assertUIObjectExists(itemContainingText(item), exists = shouldExist)
+
+ fun verifyFirstTestPageTitle(title: String) {
+ Log.i(TAG, "verifyFirstTestPageTitle: Trying to verify $title page title is visible")
+ testPageTitle()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ .check(matches(withText(title)))
+ Log.i(TAG, "verifyFirstTestPageTitle: Verified $title page title is visible")
+ }
+
+ fun verifyTestPageUrl(expectedUrl: Uri) {
+ Log.i(TAG, "verifyTestPageUrl: Trying to verify page url: $expectedUrl is displayed")
+ pageUrl(expectedUrl.toString()).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyTestPageUrl: Verified page url: $expectedUrl is displayed")
+ }
+
+ fun verifyDeleteConfirmationMessage() =
+ assertUIObjectExists(
+ itemWithResIdContainingText("$packageName:id/title", getStringResource(R.string.delete_history_prompt_title)),
+ itemWithResIdContainingText("$packageName:id/body", getStringResource(R.string.delete_history_prompt_body_2)),
+ )
+
+ fun clickDeleteHistoryButton(item: String) {
+ Log.i(TAG, "clickDeleteHistoryButton: Trying to click delete history button for item: $item")
+ deleteButton(item).click()
+ Log.i(TAG, "clickDeleteHistoryButton: Clicked delete history button for item: $item")
+ }
+
+ fun verifyDeleteHistoryItemButton(historyItemTitle: String) {
+ Log.i(TAG, "verifyDeleteHistoryItemButton: Trying to verify delete history button for item: $historyItemTitle is visible")
+ deleteButton(historyItemTitle).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeleteHistoryItemButton: Verified delete history button for item: $historyItemTitle is visible")
+ }
+
+ fun clickDeleteAllHistoryButton() {
+ Log.i(TAG, "clickDeleteAllHistoryButton: Trying to click delete all history button")
+ deleteButton().click()
+ Log.i(TAG, "clickDeleteAllHistoryButton: Clicked delete all history button")
+ }
+
+ fun selectEverythingOption() {
+ Log.i(TAG, "selectEverythingOption: Trying to click \"Everything\" dialog option")
+ deleteHistoryEverythingOption().click()
+ Log.i(TAG, "selectEverythingOption: Clicked \"Everything\" dialog option")
+ }
+
+ fun confirmDeleteAllHistory() {
+ Log.i(TAG, "confirmDeleteAllHistory: Trying to click \"Delete\" dialog button")
+ onView(withText("Delete"))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ .click()
+ Log.i(TAG, "confirmDeleteAllHistory: Clicked \"Delete\" dialog button")
+ }
+
+ fun cancelDeleteHistory() {
+ Log.i(TAG, "cancelDeleteHistory: Trying to click \"Cancel\" dialog button")
+ mDevice
+ .findObject(
+ UiSelector()
+ .textContains(getStringResource(R.string.delete_browsing_data_prompt_cancel)),
+ ).click()
+ Log.i(TAG, "cancelDeleteHistory: Clicked \"Cancel\" dialog button")
+ }
+
+ fun verifyUndoDeleteSnackBarButton() {
+ Log.i(TAG, "verifyUndoDeleteSnackBarButton: Trying to verify \"Undo\" snackbar button")
+ snackBarUndoButton().check(matches(withText("UNDO")))
+ Log.i(TAG, "verifyUndoDeleteSnackBarButton: Verified \"Undo\" snackbar button")
+ }
+
+ fun verifySearchGroupDisplayed(shouldBeDisplayed: Boolean, searchTerm: String, groupSize: Int) =
+ // checks if the search group exists in the Recently visited section
+ assertUIObjectExists(
+ itemContainingText(searchTerm)
+ .getFromParent(
+ UiSelector().text("$groupSize sites"),
+ ),
+ exists = shouldBeDisplayed,
+ )
+
+ fun openSearchGroup(searchTerm: String) {
+ Log.i(TAG, "openSearchGroup: Waiting for $waitingTime ms for search group: $searchTerm to exist")
+ mDevice.findObject(UiSelector().text(searchTerm)).waitForExists(waitingTime)
+ Log.i(TAG, "openSearchGroup: Waited for $waitingTime ms for search group: $searchTerm to exist")
+ Log.i(TAG, "openSearchGroup: Trying to click search group: $searchTerm")
+ mDevice.findObject(UiSelector().text(searchTerm)).click()
+ Log.i(TAG, "openSearchGroup: Clicked search group: $searchTerm")
+ }
+
+ class Transition {
+ fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click go back menu button")
+ onView(withContentDescription("Navigate up")).click()
+ Log.i(TAG, "goBack: Clicked go back menu button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openWebsite(url: Uri, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ assertUIObjectExists(itemWithResId("$packageName:id/history_list"))
+ Log.i(TAG, "openWebsite: Trying to click history item with url: $url")
+ onView(withText(url.toString())).click()
+ Log.i(TAG, "openWebsite: Clicked history item with url: $url")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit): RecentlyClosedTabsRobot.Transition {
+ Log.i(TAG, "openRecentlyClosedTabs: Waiting for $waitingTime ms for \"Recently closed tabs\" button to exist")
+ recentlyClosedTabsListButton().waitForExists(waitingTime)
+ Log.i(TAG, "openRecentlyClosedTabs: Waited for $waitingTime ms for \"Recently closed tabs\" button to exist")
+ Log.i(TAG, "openRecentlyClosedTabs: Trying to click \"Recently closed tabs\" button")
+ recentlyClosedTabsListButton().click()
+ Log.i(TAG, "openRecentlyClosedTabs: Clicked \"Recently closed tabs\" button")
+
+ RecentlyClosedTabsRobot().interact()
+ return RecentlyClosedTabsRobot.Transition()
+ }
+
+ fun clickSearchButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "clickSearchButton: Trying to click search history button")
+ itemWithResId("$packageName:id/history_search").click()
+ Log.i(TAG, "clickSearchButton: Clicked search history button")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+ }
+}
+
+fun historyMenu(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
+ HistoryRobot().interact()
+ return HistoryRobot.Transition()
+}
+
+private fun testPageTitle() = onView(withId(R.id.title))
+
+private fun pageUrl(url: String) = onView(allOf(withId(R.id.url), withText(url)))
+
+private fun deleteButton(title: String) =
+ onView(allOf(withContentDescription("Delete"), hasSibling(withText(title))))
+
+private fun deleteButton() = onView(withId(R.id.history_delete))
+
+private fun snackBarUndoButton() = onView(withId(R.id.snackbar_btn))
+
+private fun deleteHistoryEverythingOption() =
+ mDevice
+ .findObject(
+ UiSelector()
+ .textContains(getStringResource(R.string.delete_history_prompt_button_everything))
+ .resourceId("$packageName:id/everything_button"),
+ )
+
+private fun recentlyClosedTabsListButton() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/recently_closed_tabs_header"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt
new file mode 100644
index 0000000000..2732ccbcab
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt
@@ -0,0 +1,1171 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import android.view.View
+import android.widget.EditText
+import android.widget.TextView
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onChildAt
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove
+import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import androidx.test.uiautomator.Until.findObject
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.Matchers
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithIndex
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndIndex
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.tabstray.TabsTrayTestTag
+
+/**
+ * Implementation of Robot Pattern for the home screen menu.
+ */
+class HomeScreenRobot {
+ fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar())
+
+ fun verifyHomeScreen() = assertUIObjectExists(homeScreen())
+
+ fun verifyPrivateBrowsingHomeScreenItems() {
+ verifyHomeScreenAppBarItems()
+ assertUIObjectExists(
+ itemContainingText(
+ "$appName clears your search and browsing history from private tabs when you close them" +
+ " or quit the app. While this doesn’t make you anonymous to websites or your internet" +
+ " service provider, it makes it easier to keep what you do online private from anyone" +
+ " else who uses this device.",
+ ),
+ )
+ verifyCommonMythsLink()
+ }
+
+ fun verifyHomeScreenAppBarItems() =
+ assertUIObjectExists(homeScreen(), privateBrowsingButton(), homepageWordmark())
+
+ fun verifyHomePrivateBrowsingButton() = assertUIObjectExists(privateBrowsingButton())
+ fun verifyHomeMenuButton() = assertUIObjectExists(menuButton())
+ fun verifyTabButton() {
+ Log.i(TAG, "verifyTabButton: Trying to verify tab counter button is visible")
+ onView(allOf(withId(R.id.tab_button), isDisplayed())).check(
+ matches(
+ withEffectiveVisibility(
+ Visibility.VISIBLE,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyTabButton: Verified tab counter button is visible")
+ }
+ fun verifyCollectionsHeader() {
+ Log.i(TAG, "verifyCollectionsHeader: Trying to verify collections header is visible")
+ onView(allOf(withText("Collections"))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCollectionsHeader: Verified collections header is visible")
+ }
+ fun verifyNoCollectionsText() {
+ Log.i(TAG, "verifyNoCollectionsText: Trying to verify empty collections placeholder text is displayed")
+ onView(
+ withText(
+ containsString(
+ "Collect the things that matter to you.\n" +
+ "Group together similar searches, sites, and tabs for quick access later.",
+ ),
+ ),
+ ).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyNoCollectionsText: Verified empty collections placeholder text is displayed")
+ }
+
+ fun verifyHomeWordmark() {
+ Log.i(TAG, "verifyHomeWordmark: Trying to scroll 3x to the beginning of the home screen")
+ homeScreenList().scrollToBeginning(3)
+ Log.i(TAG, "verifyHomeWordmark: Scrolled 3x to the beginning of the home screen")
+ assertUIObjectExists(homepageWordmark())
+ }
+ fun verifyHomeComponent() {
+ Log.i(TAG, "verifyHomeComponent: Trying to verify home screen view is visible")
+ onView(ViewMatchers.withResourceName("sessionControlRecyclerView"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomeComponent: Verified home screen view is visible")
+ }
+
+ fun verifyTabCounter(numberOfOpenTabs: String) =
+ onView(
+ allOf(
+ withId(R.id.counter_text),
+ withText(numberOfOpenTabs),
+ ),
+ ).check(matches(isDisplayed()))
+
+ fun verifyWallpaperImageApplied(isEnabled: Boolean) =
+ assertUIObjectExists(itemWithResId("$packageName:id/wallpaperImageView"), exists = isEnabled)
+
+ // Upgrading users onboarding dialog
+ fun verifyUpgradingUserOnboardingFirstScreen(testRule: ComposeTestRule) {
+ testRule.also {
+ Log.i(TAG, "verifyUpgradingUserOnboardingFirstScreen: Trying to verify that the upgrading user first onboarding screen title is displayed")
+ it.onNodeWithText(getStringResource(R.string.onboarding_home_welcome_title_2))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyUpgradingUserOnboardingFirstScreen: Verified that the upgrading user first onboarding screen title is displayed")
+ Log.i(TAG, "verifyUpgradingUserOnboardingFirstScreen: Trying to verify that the upgrading user first onboarding screen description is displayed")
+ it.onNodeWithText(getStringResource(R.string.onboarding_home_welcome_description))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyUpgradingUserOnboardingFirstScreen: Verified that the upgrading user first onboarding screen description is displayed")
+ Log.i(TAG, "verifyUpgradingUserOnboardingFirstScreen: Trying to verify that the upgrading user first onboarding \"Get started\" button is displayed")
+ it.onNodeWithText(getStringResource(R.string.onboarding_home_get_started_button))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyUpgradingUserOnboardingFirstScreen: Verified that the upgrading user first onboarding \"Get started\" button is displayed")
+ }
+ }
+
+ fun verifyFirstOnboardingCard(composeTestRule: ComposeTestRule) {
+ composeTestRule.also {
+ Log.i(TAG, "verifyFirstOnboardingCard: Trying to verify that the first onboarding screen title exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_default_browser_title_nimbus_2),
+ ).assertExists()
+ Log.i(TAG, "verifyFirstOnboardingCard: Verified that the first onboarding screen title exists")
+ Log.i(TAG, "verifyFirstOnboardingCard: Trying to verify that the first onboarding screen description exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_3),
+ ).assertExists()
+ Log.i(TAG, "verifyFirstOnboardingCard: Verified that the first onboarding screen description exists")
+ Log.i(TAG, "verifyFirstOnboardingCard: Trying to verify that the first onboarding \"Set as default browser\" button exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_default_browser_positive_button),
+ ).assertExists()
+ Log.i(TAG, "verifyFirstOnboardingCard: Verified that the first onboarding \"Set as default browser\" button exists")
+ Log.i(TAG, "verifyFirstOnboardingCard: Trying to verify that the first onboarding \"Not now\" button exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_default_browser_negative_button),
+ ).assertExists()
+ Log.i(TAG, "verifyFirstOnboardingCard: Verified that the first onboarding \"Not now\" button exists")
+ }
+ }
+
+ fun verifySecondOnboardingCard(composeTestRule: ComposeTestRule) {
+ composeTestRule.also {
+ Log.i(TAG, "verifySecondOnboardingCard: Trying to verify that the second onboarding screen title exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_add_search_widget_title),
+ ).assertExists()
+ Log.i(TAG, "verifySecondOnboardingCard: Verified that the second onboarding screen title exists")
+ Log.i(TAG, "verifySecondOnboardingCard: Trying to verify that the second onboarding screen description exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_add_search_widget_description),
+ ).assertExists()
+ Log.i(TAG, "verifySecondOnboardingCard: Verified that the second onboarding screen description exists")
+ Log.i(TAG, "verifySecondOnboardingCard: Trying to verify that the first onboarding \"Sign in\" button exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_add_search_widget_positive_button),
+ ).assertExists()
+ Log.i(TAG, "verifySecondOnboardingCard: Verified that the first onboarding \"Add Firefox widget\" button exists")
+ Log.i(TAG, "verifySecondOnboardingCard: Trying to verify that the second onboarding \"Not now\" button exists")
+ it.onNodeWithTag(
+ getStringResource(R.string.juno_onboarding_add_search_widget_title) + "onboarding_card.negative_button",
+ ).assertExists()
+ Log.i(TAG, "verifySecondOnboardingCard: Verified that the second onboarding \"Not now\" button exists")
+ }
+ }
+
+ fun verifyThirdOnboardingCard(composeTestRule: ComposeTestRule) {
+ composeTestRule.also {
+ Log.i(TAG, "verifyThirdOnboardingCard: Trying to verify that the third onboarding screen title exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_sign_in_title_2),
+ ).assertExists()
+ Log.i(TAG, "verifyThirdOnboardingCard: Verified that the third onboarding screen title exists")
+ Log.i(TAG, "verifyThirdOnboardingCard: Trying to verify that the third onboarding screen description exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_sign_in_description_2),
+ ).assertExists()
+ Log.i(TAG, "verifyThirdOnboardingCard: Verified that the third onboarding screen description exists")
+ Log.i(TAG, "verifyThirdOnboardingCard: Trying to verify that the first onboarding \"Sign in\" button exists")
+ it.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_sign_in_positive_button),
+ ).assertExists()
+ Log.i(TAG, "verifyThirdOnboardingCard: Verified that the first onboarding \"Sign in\" button exists")
+ Log.i(TAG, "verifyThirdOnboardingCard: Trying to verify that the third onboarding \"Not now\" button exists")
+ it.onNodeWithTag(
+ getStringResource(R.string.juno_onboarding_sign_in_title_2) + "onboarding_card.negative_button",
+ ).assertExists()
+ Log.i(TAG, "verifySecondOnboardingCard: Verified that the third onboarding \"Not now\" button exists")
+ }
+ }
+
+ fun clickDefaultCardNotNowOnboardingButton(composeTestRule: ComposeTestRule) {
+ Log.i(TAG, "clickNotNowOnboardingButton: Trying to click \"Not now\" onboarding button")
+ composeTestRule.onNodeWithTag(
+ getStringResource(R.string.juno_onboarding_default_browser_title_nimbus_2) + "onboarding_card.negative_button",
+ ).performClick()
+ Log.i(TAG, "clickNotNowOnboardingButton: Clicked \"Not now\" onboarding button")
+ }
+
+ fun clickAddSearchWidgetNotNowOnboardingButton(composeTestRule: ComposeTestRule) {
+ Log.i(TAG, "clickNotNowOnboardingButton: Trying to click \"Not now\" onboarding button")
+ composeTestRule.onNodeWithTag(
+ getStringResource(R.string.juno_onboarding_add_search_widget_title) + "onboarding_card.negative_button",
+ ).performClick()
+ Log.i(TAG, "clickNotNowOnboardingButton: Clicked \"Not now\" onboarding button")
+ }
+
+ fun clickSyncSignInWidgetNotNowOnboardingButton(composeTestRule: ComposeTestRule) {
+ Log.i(TAG, "clickNotNowOnboardingButton: Trying to click \"Not now\" onboarding button")
+ composeTestRule.onNodeWithTag(
+ getStringResource(R.string.juno_onboarding_sign_in_title_2) + "onboarding_card.negative_button",
+ ).performClick()
+ Log.i(TAG, "clickNotNowOnboardingButton: Clicked \"Not now\" onboarding button")
+ }
+
+ fun swipeSecondOnboardingCardToRight() {
+ Log.i(TAG, "swipeSecondOnboardingCardToRight: Trying to perform swipe right action on second onboarding card")
+ mDevice.findObject(
+ UiSelector().textContains(
+ getStringResource(R.string.juno_onboarding_sign_in_title_2),
+ ),
+ ).swipeRight(3)
+ Log.i(TAG, "swipeSecondOnboardingCardToRight: Performed swipe right action on second onboarding card")
+ }
+
+ fun clickGetStartedButton(testRule: ComposeTestRule) {
+ Log.i(TAG, "clickGetStartedButton: Trying to click \"Get started\" onboarding button")
+ testRule.onNodeWithText(getStringResource(R.string.onboarding_home_get_started_button))
+ .performClick()
+ Log.i(TAG, "clickGetStartedButton: Clicked \"Get started\" onboarding button")
+ }
+
+ fun clickCloseButton(testRule: ComposeTestRule) {
+ Log.i(TAG, "clickCloseButton: Trying to click close onboarding button")
+ testRule.onNode(hasContentDescription("Close")).performClick()
+ Log.i(TAG, "clickCloseButton: Clicked close onboarding button")
+ }
+
+ fun verifyUpgradingUserOnboardingSecondScreen(testRule: ComposeTestRule) {
+ testRule.also {
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Trying to verify that the upgrading user second onboarding screen title is displayed")
+ it.onNodeWithText(getStringResource(R.string.onboarding_home_sync_title_3))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Verified that the upgrading user second onboarding screen title is displayed")
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Trying to verify that the upgrading user second onboarding screen description is displayed")
+ it.onNodeWithText(getStringResource(R.string.onboarding_home_sync_description))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Verified that the upgrading user second onboarding screen description is displayed")
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Trying to verify that the upgrading user second onboarding \"Sign in\" button is displayed")
+ it.onNodeWithText(getStringResource(R.string.onboarding_home_sign_in_button))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Verified that the upgrading user second onboarding \"Sign in\" button is displayed")
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Trying to that the verify upgrading user second onboarding \"Skip\" button is displayed")
+ it.onNodeWithText(getStringResource(R.string.onboarding_home_skip_button))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyUpgradingUserOnboardingSecondScreen: Verified that the upgrading user second onboarding \"Skip\" button is displayed")
+ }
+ }
+
+ fun clickSkipButton(testRule: ComposeTestRule) {
+ Log.i(TAG, "clickSkipButton: Trying to click \"Skip\" onboarding button")
+ testRule
+ .onNodeWithText(getStringResource(R.string.onboarding_home_skip_button))
+ .performClick()
+ Log.i(TAG, "clickSkipButton: Clicked \"Skip\" onboarding button")
+ }
+
+ fun verifyCommonMythsLink() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.private_browsing_common_myths)))
+
+ fun verifyExistingTopSitesList() =
+ assertUIObjectExists(itemWithResId("$packageName:id/top_sites_list"))
+
+ fun verifyNotExistingTopSitesList(title: String) {
+ Log.i(TAG, "verifyNotExistingTopSitesList: Waiting for $waitingTime ms for top site: $title to be gone")
+ mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime)
+ Log.i(TAG, "verifyNotExistingTopSitesList: Waited for $waitingTime ms for top site: $title to be gone")
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/top_site_title",
+ title,
+ ),
+ exists = false,
+ )
+ }
+ fun verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle: String, position: Int) =
+ assertUIObjectExists(
+ itemWithResIdAndIndex("$packageName:id/top_site_item", index = position - 1)
+ .getChild(
+ UiSelector()
+ .textContains(sponsoredShortcutTitle),
+ ),
+ exists = false,
+ )
+ fun verifyNotExistingSponsoredTopSitesList() =
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/top_site_subtitle",
+ getStringResource(R.string.top_sites_sponsored_label),
+ ),
+ exists = false,
+ )
+
+ fun verifyExistingTopSitesTabs(title: String) {
+ Log.i(TAG, "verifyExistingTopSitesTabs: Trying to scroll into view the top sites list")
+ homeScreenList().scrollIntoView(itemWithResId("$packageName:id/top_sites_list"))
+ Log.i(TAG, "verifyExistingTopSitesTabs: Scrolled into view the top sites list")
+ Log.i(TAG, "verifyExistingTopSitesTabs: Waiting for $waitingTime ms for top site: $title to exist")
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/top_site_title")
+ .textContains(title),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifyExistingTopSitesTabs: Waited for $waitingTime ms for top site: $title to exist")
+ Log.i(TAG, "verifyExistingTopSitesTabs: Trying to verify top site: $title is visible")
+ onView(allOf(withId(R.id.top_sites_list)))
+ .check(matches(hasDescendant(withText(title))))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyExistingTopSitesTabs: Verified top site: $title is visible")
+ }
+ fun verifySponsoredShortcutDetails(sponsoredShortcutTitle: String, position: Int) {
+ assertUIObjectExists(
+ itemWithResIdAndIndex(resourceId = "$packageName:id/top_site_item", index = position - 1)
+ .getChild(
+ UiSelector()
+ .resourceId("$packageName:id/favicon_card"),
+ ),
+ )
+ assertUIObjectExists(
+ itemWithResIdAndIndex(resourceId = "$packageName:id/top_site_item", index = position - 1)
+ .getChild(
+ UiSelector()
+ .textContains(sponsoredShortcutTitle),
+ ),
+ )
+ assertUIObjectExists(
+ itemWithResIdAndIndex(resourceId = "$packageName:id/top_site_item", index = position - 1)
+ .getChild(
+ UiSelector()
+ .resourceId("$packageName:id/top_site_subtitle"),
+ ),
+ )
+ }
+ fun verifyTopSiteContextMenuItems() {
+ mDevice.waitNotNull(
+ findObject(By.text("Open in private tab")),
+ waitingTime,
+ )
+ mDevice.waitNotNull(
+ findObject(By.text("Remove")),
+ waitingTime,
+ )
+ }
+
+ fun verifyJumpBackInSectionIsDisplayed() {
+ scrollToElementByText(getStringResource(R.string.recent_tabs_header))
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.recent_tabs_header)))
+ }
+ fun verifyJumpBackInSectionIsNotDisplayed() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.recent_tabs_header)), exists = false)
+ fun verifyJumpBackInItemTitle(testRule: ComposeTestRule, itemTitle: String) {
+ Log.i(TAG, "verifyJumpBackInItemTitle: Trying to verify jump back in item with title: $itemTitle")
+ testRule.onNodeWithTag("recent.tab.title", useUnmergedTree = true)
+ .assert(hasText(itemTitle))
+ Log.i(TAG, "verifyJumpBackInItemTitle: Verified jump back in item with title: $itemTitle")
+ }
+ fun verifyJumpBackInItemWithUrl(testRule: ComposeTestRule, itemUrl: String) {
+ Log.i(TAG, "verifyJumpBackInItemWithUrl: Trying to verify jump back in item with URL: $itemUrl")
+ testRule.onNodeWithTag("recent.tab.url", useUnmergedTree = true).assert(hasText(itemUrl))
+ Log.i(TAG, "verifyJumpBackInItemWithUrl: Verified jump back in item with URL: $itemUrl")
+ }
+ fun verifyJumpBackInShowAllButton() = assertUIObjectExists(itemContainingText(getStringResource(R.string.recent_tabs_show_all)))
+ fun verifyRecentlyVisitedSectionIsDisplayed(exists: Boolean) =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.history_metadata_header_2)), exists = exists)
+ fun verifyRecentBookmarksSectionIsDisplayed(exists: Boolean) =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.recently_saved_title)), exists = exists)
+
+ fun verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed: Boolean, searchTerm: String, groupSize: Int) {
+ // checks if the search group exists in the Recently visited section
+ if (shouldBeDisplayed) {
+ scrollToElementByText("Recently visited")
+ assertUIObjectExists(
+ itemContainingText(searchTerm)
+ .getFromParent(UiSelector().text("$groupSize sites")),
+ )
+ } else {
+ assertUIObjectIsGone(
+ itemContainingText(searchTerm)
+ .getFromParent(UiSelector().text("$groupSize sites")),
+ )
+ }
+ }
+
+ // Collections elements
+ fun verifyCollectionIsDisplayed(title: String, collectionExists: Boolean = true) {
+ if (collectionExists) {
+ assertUIObjectExists(itemContainingText(title))
+ } else {
+ assertUIObjectIsGone(itemWithText(title))
+ }
+ }
+
+ fun togglePrivateBrowsingModeOnOff() {
+ Log.i(TAG, "togglePrivateBrowsingModeOnOff: Trying to click private browsing home screen button")
+ onView(ViewMatchers.withResourceName("privateBrowsingButton"))
+ .perform(click())
+ Log.i(TAG, "togglePrivateBrowsingModeOnOff: Clicked private browsing home screen button")
+ }
+
+ fun verifyThoughtProvokingStories(enabled: Boolean) {
+ if (enabled) {
+ scrollToElementByText(getStringResource(R.string.pocket_stories_header_1))
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_header_1)))
+ } else {
+ Log.i(TAG, "verifyThoughtProvokingStories: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyThoughtProvokingStories: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_header_1)), exists = false)
+ }
+ }
+
+ fun scrollToPocketProvokingStories() {
+ Log.i(TAG, "scrollToPocketProvokingStories: Trying to scroll into view the featured pocket stories")
+ homeScreenList().scrollIntoView(
+ mDevice.findObject(UiSelector().resourceId("pocket.recommended.story").index(2)),
+ )
+ Log.i(TAG, "scrollToPocketProvokingStories: Scrolled into view the featured pocket stories")
+ }
+
+ fun verifyPocketRecommendedStoriesItems() {
+ for (position in 0..8) {
+ Log.i(TAG, "verifyPocketRecommendedStoriesItems: Trying to scroll into view the featured pocket story from position: $position")
+ pocketStoriesList().scrollIntoView(UiSelector().index(position))
+ Log.i(TAG, "verifyPocketRecommendedStoriesItems: Scrolled into view the featured pocket story from position: $position")
+ assertUIObjectExists(itemWithIndex(position))
+ }
+ }
+
+ // Temporarily not in use because Sponsored Pocket stories are only advertised for a limited time.
+ // See also known issue https://bugzilla.mozilla.org/show_bug.cgi?id=1828629
+// fun verifyPocketSponsoredStoriesItems(vararg positions: Int) {
+// positions.forEach {
+// pocketStoriesList
+// .scrollIntoView(UiSelector().resourceId("pocket.sponsored.story").index(it - 1))
+//
+// assertTrue(
+// "Pocket story item at position $it not found.",
+// mDevice.findObject(UiSelector().index(it - 1).resourceId("pocket.sponsored.story"))
+// .waitForExists(waitingTimeShort),
+// )
+// }
+// }
+
+ fun verifyDiscoverMoreStoriesButton() {
+ Log.i(TAG, "verifyDiscoverMoreStoriesButton: Trying to scroll into view the Pocket \"Discover more\" button")
+ pocketStoriesList().scrollIntoView(UiSelector().text("Discover more"))
+ Log.i(TAG, "verifyDiscoverMoreStoriesButton: Scrolled into view the Pocket \"Discover more\" button")
+ assertUIObjectExists(itemWithText("Discover more"))
+ }
+
+ fun verifyStoriesByTopic(enabled: Boolean) {
+ if (enabled) {
+ scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header))
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_categories_header)))
+ } else {
+ Log.i(TAG, "verifyStoriesByTopic: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyStoriesByTopic: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_categories_header)), exists = false)
+ }
+ }
+
+ fun verifyStoriesByTopicItems() {
+ Log.i(TAG, "verifyStoriesByTopicItems: Trying to scroll into view the stories by topic home screen section")
+ homeScreenList().scrollIntoView(UiSelector().resourceId("pocket.categories"))
+ Log.i(TAG, "verifyStoriesByTopicItems: Scrolled into view the stories by topic home screen section")
+ Log.i(TAG, "verifyStoriesByTopicItems: Trying to verify that there are more than 1 \"Stories by topic\" categories")
+ assertTrue(mDevice.findObject(UiSelector().resourceId("pocket.categories")).childCount > 1)
+ Log.i(TAG, "verifyStoriesByTopicItems: Verified that there are more than 1 \"Stories by topic\" categories")
+ }
+
+ fun verifyStoriesByTopicItemState(composeTestRule: ComposeTestRule, isSelected: Boolean, position: Int) {
+ Log.i(TAG, "verifyStoriesByTopicItemState: Trying to scroll into view \"Powered By Pocket\" home screen section")
+ homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))
+ Log.i(TAG, "verifyStoriesByTopicItemState: Scrolled into view \"Powered By Pocket\" home screen section")
+
+ if (isSelected) {
+ Log.i(TAG, "verifyStoriesByTopicItemState: Trying verify that the stories by topic home screen section is displayed")
+ composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed()
+ Log.i(TAG, "verifyStoriesByTopicItemState: Verified that the stories by topic home screen section is displayed")
+ Log.i(TAG, "verifyStoriesByTopicItemState: Trying verify that the stories by topic item at position: $position is selected")
+ storyByTopicItem(composeTestRule, position).assertIsSelected()
+ Log.i(TAG, "verifyStoriesByTopicItemState: Verified that the stories by topic item at position: $position is selected")
+ } else {
+ Log.i(TAG, "verifyStoriesByTopicItemState: Trying verify that the stories by topic home screen section is displayed")
+ composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed()
+ Log.i(TAG, "verifyStoriesByTopicItemState: Verified that the stories by topic home screen section is displayed")
+ Log.i(TAG, "verifyStoriesByTopicItemState: Trying to verify that the stories by topic item at position: $position is not selected")
+ storyByTopicItem(composeTestRule, position).assertIsNotSelected()
+ Log.i(TAG, "verifyStoriesByTopicItemState: Verified that the stories by topic item at position: $position is not selected")
+ }
+ }
+
+ fun clickStoriesByTopicItem(composeTestRule: ComposeTestRule, position: Int) {
+ Log.i(TAG, "clickStoriesByTopicItem: Trying to click stories by topic item from position: $position")
+ storyByTopicItem(composeTestRule, position).performClick()
+ Log.i(TAG, "clickStoriesByTopicItem: Clicked stories by topic item from position: $position")
+ }
+
+ fun verifyPoweredByPocket() {
+ Log.i(TAG, "verifyPoweredByPocket: Trying to scroll into view \"Powered By Pocket\" home screen section")
+ homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))
+ Log.i(TAG, "verifyPoweredByPocket: Scrolled into view \"Powered By Pocket\" home screen section")
+ assertUIObjectExists(itemWithResId("pocket.header.title"))
+ }
+
+ fun verifyCustomizeHomepageButton(enabled: Boolean) {
+ if (enabled) {
+ scrollToElementByText(getStringResource(R.string.browser_menu_customize_home_1))
+ assertUIObjectExists(itemContainingText("Customize homepage"))
+ } else {
+ Log.i(TAG, "verifyCustomizeHomepageButton: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyCustomizeHomepageButton: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ assertUIObjectExists(itemContainingText("Customize homepage"), exists = false)
+ }
+ }
+
+ fun verifyJumpBackInMessage(composeTestRule: ComposeTestRule) {
+ Log.i(TAG, "verifyJumpBackInMessage: Trying to verify jump back in contextual message")
+ composeTestRule
+ .onNodeWithText(
+ getStringResource(R.string.onboarding_home_screen_jump_back_contextual_hint_2),
+ ).assertExists()
+ Log.i(TAG, "verifyJumpBackInMessage: Verified jump back in contextual message")
+ }
+
+ fun getProvokingStoryPublisher(position: Int): String {
+ val publisher = mDevice.findObject(
+ UiSelector()
+ .className("android.view.View")
+ .index(position - 1),
+ ).getChild(
+ UiSelector()
+ .className("android.widget.TextView")
+ .index(1),
+ ).text
+
+ return publisher
+ }
+
+ fun verifyToolbarPosition(defaultPosition: Boolean) {
+ Log.i(TAG, "verifyToolbarPosition: Trying to verify toolbar is set to top: $defaultPosition")
+ onView(withId(R.id.toolbarLayout))
+ .check(
+ if (defaultPosition) {
+ isPartiallyBelow(withId(R.id.sessionControlRecyclerView))
+ } else {
+ isCompletelyAbove(withId(R.id.homeAppBar))
+ },
+ )
+ Log.i(TAG, "verifyToolbarPosition: Verified toolbar position is set to top: $defaultPosition")
+ }
+ fun verifyNimbusMessageCard(title: String, text: String, action: String) {
+ val textView = UiSelector()
+ .className(ComposeView::class.java)
+ .className(View::class.java)
+ .className(TextView::class.java)
+ assertTrue(
+ mDevice.findObject(textView.textContains(title)).waitForExists(waitingTime),
+ )
+ assertTrue(
+ mDevice.findObject(textView.textContains(text)).waitForExists(waitingTime),
+ )
+ assertTrue(
+ mDevice.findObject(textView.textContains(action)).waitForExists(waitingTime),
+ )
+ }
+
+ fun verifyIfInPrivateOrNormalMode(privateBrowsingEnabled: Boolean) {
+ Log.i(TAG, "verifyIfInPrivateOrNormalMode: Trying to verify private browsing mode is enabled")
+ assert(isPrivateModeEnabled() == privateBrowsingEnabled)
+ Log.i(TAG, "verifyIfInPrivateOrNormalMode: Verified private browsing mode is enabled: $privateBrowsingEnabled")
+ }
+
+ class Transition {
+
+ fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "openTabDrawer: Waiting for $waitingTime ms for tab counter button to exist")
+ mDevice.findObject(
+ UiSelector().descriptionContains("open tab. Tap to switch tabs."),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "openTabDrawer: Waited for $waitingTime ms for tab counter button to exist")
+ Log.i(TAG, "openTabDrawer: Trying to click tab counter button")
+ tabsCounter().click()
+ Log.i(TAG, "openTabDrawer: Clicked tab counter button")
+ mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")))
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun openComposeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "openComposeTabDrawer: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "openComposeTabDrawer: Device was idle for $waitingTime ms")
+ Log.i(TAG, "openComposeTabDrawer: Trying to click tab counter button")
+ onView(withId(R.id.tab_button)).click()
+ Log.i(TAG, "openComposeTabDrawer: Clicked tab counter button")
+ Log.i(TAG, "openComposeTabDrawer: Trying to verify the tabs tray exists")
+ composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
+ Log.i(TAG, "openComposeTabDrawer: Verified the tabs tray exists")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
+ // Issue: https://github.com/mozilla-mobile/fenix/issues/21578
+ try {
+ Log.i(TAG, "openThreeDotMenu: Try block")
+ mDevice.waitNotNull(
+ Until.findObject(By.res("$packageName:id/menuButton")),
+ waitingTime,
+ )
+ } catch (e: AssertionError) {
+ Log.i(TAG, "openThreeDotMenu: Catch block")
+ Log.i(TAG, "openThreeDotMenu: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "openThreeDotMenu: Clicked device back button")
+ } finally {
+ Log.i(TAG, "openThreeDotMenu: Finally block")
+ Log.i(TAG, "openThreeDotMenu: Trying to click main menu button")
+ threeDotButton().perform(click())
+ Log.i(TAG, "openThreeDotMenu: Clicked main menu button")
+ }
+
+ ThreeDotMenuMainRobot().interact()
+ return ThreeDotMenuMainRobot.Transition()
+ }
+
+ fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "openSearch: Waiting for $waitingTime ms for the navigation toolbar to exist")
+ navigationToolbar().waitForExists(waitingTime)
+ Log.i(TAG, "openSearch: Waited for $waitingTime ms for the navigation toolbar to exist")
+ Log.i(TAG, "openSearch: Trying to click navigation toolbar")
+ navigationToolbar().click()
+ Log.i(TAG, "openSearch: Clicked navigation toolbar")
+ Log.i(TAG, "openSearch: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "openSearch: Device was idle")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+
+ fun clickUpgradingUserOnboardingSignInButton(
+ testRule: ComposeTestRule,
+ interact: SyncSignInRobot.() -> Unit,
+ ): SyncSignInRobot.Transition {
+ Log.i(TAG, "clickUpgradingUserOnboardingSignInButton: Trying to click the upgrading user onboarding \"Sign in\" button")
+ testRule.onNodeWithText("Sign in").performClick()
+ Log.i(TAG, "clickUpgradingUserOnboardingSignInButton: Clicked the upgrading user onboarding \"Sign in\" button")
+
+ SyncSignInRobot().interact()
+ return SyncSignInRobot.Transition()
+ }
+
+ fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) {
+ // Switch to private browsing homescreen
+ if (switchPBModeOn && !isPrivateModeEnabled()) {
+ Log.i(TAG, "togglePrivateBrowsingMode: Waiting for $waitingTime ms for private browsing button to exist")
+ privateBrowsingButton().waitForExists(waitingTime)
+ Log.i(TAG, "togglePrivateBrowsingMode: Waited for $waitingTime ms for private browsing button to exist")
+ Log.i(TAG, "togglePrivateBrowsingMode: Trying to click private browsing button")
+ privateBrowsingButton().click()
+ Log.i(TAG, "togglePrivateBrowsingMode: Clicked private browsing button")
+ }
+
+ // Switch to normal browsing homescreen
+ if (!switchPBModeOn && isPrivateModeEnabled()) {
+ Log.i(TAG, "togglePrivateBrowsingMode: Waiting for $waitingTime ms for private browsing button to exist")
+ privateBrowsingButton().waitForExists(waitingTime)
+ Log.i(TAG, "togglePrivateBrowsingMode: Waited for $waitingTime ms for private browsing button to exist")
+ Log.i(TAG, "togglePrivateBrowsingMode: Trying to click private browsing button")
+ privateBrowsingButton().click()
+ privateBrowsingButton().click()
+ Log.i(TAG, "togglePrivateBrowsingMode: Clicked private browsing button")
+ }
+ }
+
+ fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
+ // Loop to press the PB icon for 5 times to display the Add the Private Browsing Shortcut CFR
+ for (i in 1..5) {
+ Log.i(TAG, "triggerPrivateBrowsingShortcutPrompt: Waiting for $waitingTime ms for private browsing button to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
+ .waitForExists(
+ waitingTime,
+ )
+ Log.i(TAG, "triggerPrivateBrowsingShortcutPrompt: Waited for $waitingTime ms for private browsing button to exist")
+ Log.i(TAG, "triggerPrivateBrowsingShortcutPrompt: Trying to click private browsing button")
+ privateBrowsingButton().click()
+ Log.i(TAG, "triggerPrivateBrowsingShortcutPrompt: Clicked private browsing button")
+ }
+
+ AddToHomeScreenRobot().interact()
+ return AddToHomeScreenRobot.Transition()
+ }
+
+ fun pressBack() {
+ Log.i(TAG, "pressBack: Trying to click device back button")
+ onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack())
+ Log.i(TAG, "pressBack: Clicked device back button")
+ }
+
+ fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
+ Log.i(TAG, "openNavigationToolbar: Waiting for $waitingTime ms for navigation the toolbar to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "openNavigationToolbar: Waited for $waitingTime ms for the navigation toolbar to exist")
+ Log.i(TAG, "openNavigationToolbar: Trying to click the navigation toolbar")
+ navigationToolbar().click()
+ Log.i(TAG, "openNavigationToolbar: Clicked the navigation toolbar")
+
+ NavigationToolbarRobot().interact()
+ return NavigationToolbarRobot.Transition()
+ }
+
+ fun openContextMenuOnTopSitesWithTitle(
+ title: String,
+ interact: HomeScreenRobot.() -> Unit,
+ ): Transition {
+ Log.i(TAG, "openContextMenuOnTopSitesWithTitle: Trying to long click top site with title: $title")
+ onView(withId(R.id.top_sites_list)).perform(
+ actionOnItem(
+ hasDescendant(withText(title)),
+ ViewActions.longClick(),
+ ),
+ )
+ Log.i(TAG, "openContextMenuOnTopSitesWithTitle: Long clicked top site with title: $title")
+
+ HomeScreenRobot().interact()
+ return Transition()
+ }
+
+ fun openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle: String, interact: HomeScreenRobot.() -> Unit): Transition {
+ Log.i(TAG, "openContextMenuOnSponsoredShortcut: Trying to long click: $sponsoredShortcutTitle sponsored shortcut")
+ sponsoredShortcut(sponsoredShortcutTitle).perform(longClick())
+ Log.i(TAG, "openContextMenuOnSponsoredShortcut: Long clicked: $sponsoredShortcutTitle sponsored shortcut")
+
+ HomeScreenRobot().interact()
+ return Transition()
+ }
+
+ fun openTopSiteTabWithTitle(
+ title: String,
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ Log.i(TAG, "openTopSiteTabWithTitle: Trying to click top site with title $title")
+ onView(withId(R.id.top_sites_list)).perform(
+ actionOnItem(hasDescendant(withText(title)), click()),
+ )
+ Log.i(TAG, "openTopSiteTabWithTitle:Clicked top site with title $title")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openSponsoredShortcut(sponsoredShortcutTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openSponsoredShortcut: Trying to click sponsored top site with title: $sponsoredShortcutTitle")
+ sponsoredShortcut(sponsoredShortcutTitle).click()
+ Log.i(TAG, "openSponsoredShortcut: Clicked sponsored top site with title: $sponsoredShortcutTitle")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun renameTopSite(title: String, interact: HomeScreenRobot.() -> Unit): Transition {
+ Log.i(TAG, "renameTopSite: Trying to click context menu \"Rename\" button")
+ onView(withText("Rename"))
+ .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
+ .perform(click())
+ Log.i(TAG, "renameTopSite: Clicked context menu \"Rename\" button")
+ Log.i(TAG, "renameTopSite: Trying to set top site title to: $title")
+ onView(Matchers.allOf(withId(R.id.top_site_title), instanceOf(EditText::class.java)))
+ .perform(ViewActions.replaceText(title))
+ Log.i(TAG, "renameTopSite: Set top site title to: $title")
+ Log.i(TAG, "renameTopSite: Trying to click \"Ok\" rename top site dialog button")
+ onView(withId(android.R.id.button1)).perform((click()))
+ Log.i(TAG, "renameTopSite: Clicked \"Ok\" rename top site dialog button")
+
+ HomeScreenRobot().interact()
+ return Transition()
+ }
+
+ fun removeTopSite(interact: HomeScreenRobot.() -> Unit): Transition {
+ Log.i(TAG, "removeTopSite: Trying to click context menu \"Remove\" button")
+ onView(withText("Remove"))
+ .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
+ .perform(click())
+ Log.i(TAG, "removeTopSite: Clicked context menu \"Remove\" button")
+
+ HomeScreenRobot().interact()
+ return Transition()
+ }
+
+ fun deleteTopSiteFromHistory(interact: HomeScreenRobot.() -> Unit): Transition {
+ Log.i(TAG, "deleteTopSiteFromHistory: Waiting for $waitingTime ms for context menu \"Remove from history\" button to exist")
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/simple_text"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "deleteTopSiteFromHistory: Waited for $waitingTime ms for context menu \"Remove from history\" button to exist")
+ Log.i(TAG, "deleteTopSiteFromHistory: Trying to click context menu \"Remove from history\" button")
+ deleteFromHistory().click()
+ Log.i(TAG, "deleteTopSiteFromHistory: Clicked context menu \"Remove from history\" button")
+
+ HomeScreenRobot().interact()
+ return Transition()
+ }
+
+ fun openTopSiteInPrivateTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openTopSiteInPrivateTab: Trying to click context menu \"Open in private tab\" button")
+ onView(withText("Open in private tab"))
+ .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
+ .perform(click())
+ Log.i(TAG, "openTopSiteInPrivateTab: Clicked context menu \"Open in private tab\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickSponsorsAndPrivacyButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickSponsorsAndPrivacyButton: Waiting for $waitingTime ms for context menu \"Our sponsors & your privacy\" button to exist")
+ sponsorsAndPrivacyButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickSponsorsAndPrivacyButton: Waited for $waitingTime ms for context menu \"Our sponsors & your privacy\" button to exist")
+ Log.i(TAG, "clickSponsorsAndPrivacyButton: Trying to click \"Our sponsors & your privacy\" context menu button and wait for $waitingTime ms for a new window")
+ sponsorsAndPrivacyButton().clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickSponsorsAndPrivacyButton: Clicked \"Our sponsors & your privacy\" context menu button and waited for $waitingTime ms for a new window")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
+ Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Waiting for $waitingTime ms for context menu \"Settings\" button to exist")
+ sponsoredShortcutsSettingsButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Waited for $waitingTime ms for context menu \"Settings\" button to exist")
+ Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Trying to click \"Settings\" context menu button and wait for $waitingTime for a new window")
+ sponsoredShortcutsSettingsButton().clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Clicked \"Settings\" context menu button and waited for $waitingTime for a new window")
+
+ SettingsSubMenuHomepageRobot().interact()
+ return SettingsSubMenuHomepageRobot.Transition()
+ }
+
+ fun openCommonMythsLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openCommonMythsLink: Trying to click private browsing home screen common myths link")
+ mDevice.findObject(
+ UiSelector()
+ .textContains(
+ getStringResource(R.string.private_browsing_common_myths),
+ ),
+ ).also { it.click() }
+ Log.i(TAG, "openCommonMythsLink: Clicked private browsing home screen common myths link")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickSaveTabsToCollectionButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ scrollToElementByText(getStringResource(R.string.no_collections_description2))
+ Log.i(TAG, "clickSaveTabsToCollectionButton: Trying to click save tabs to collection button")
+ saveTabsToCollectionButton().click()
+ Log.i(TAG, "clickSaveTabsToCollectionButton: Clicked save tabs to collection button")
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickSaveTabsToCollectionButton(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ scrollToElementByText(getStringResource(R.string.no_collections_description2))
+ Log.i(TAG, "clickSaveTabsToCollectionButton: Trying to click save tabs to collection button")
+ saveTabsToCollectionButton().click()
+ Log.i(TAG, "clickSaveTabsToCollectionButton: Clicked save tabs to collection button")
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun expandCollection(title: String, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
+ assertUIObjectExists(itemContainingText(title))
+ Log.i(TAG, "expandCollection: Trying to click collection with title: $title and wait for $waitingTimeShort ms for a new window")
+ itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "expandCollection: Clicked collection with title: $title and waited for $waitingTimeShort ms for a new window")
+ assertUIObjectExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection)))
+
+ CollectionRobot().interact()
+ return CollectionRobot.Transition()
+ }
+
+ fun openRecentlyVisitedSearchGroupHistoryList(title: String, interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
+ scrollToElementByText("Recently visited")
+ val searchGroup = mDevice.findObject(UiSelector().text(title))
+ Log.i(TAG, "openRecentlyVisitedSearchGroupHistoryList: Waiting for $waitingTimeShort ms for recently visited search group with title: $title to exist")
+ searchGroup.waitForExists(waitingTimeShort)
+ Log.i(TAG, "openRecentlyVisitedSearchGroupHistoryList: Waited for $waitingTimeShort ms for recently visited search group with title: $title to exist")
+ Log.i(TAG, "openRecentlyVisitedSearchGroupHistoryList: Trying to click recently visited search group with title: $title")
+ searchGroup.click()
+ Log.i(TAG, "openRecentlyVisitedSearchGroupHistoryList: Clicked recently visited search group with title: $title")
+
+ HistoryRobot().interact()
+ return HistoryRobot.Transition()
+ }
+
+ fun openCustomizeHomepage(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
+ Log.i(TAG, "openCustomizeHomepage: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "openCustomizeHomepage: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the home screen")
+ Log.i(TAG, "openCustomizeHomepage: Trying to click \"Customize homepage\" button and wait for $waitingTime ms for a new window")
+ mDevice.findObject(
+ UiSelector()
+ .textContains(
+ "Customize homepage",
+ ),
+ ).clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "openCustomizeHomepage: Clicked \"Customize homepage\" button and wait for $waitingTime ms for a new window")
+
+ SettingsSubMenuHomepageRobot().interact()
+ return SettingsSubMenuHomepageRobot.Transition()
+ }
+
+ fun clickJumpBackInShowAllButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "clickJumpBackInShowAllButton: Trying to click \"Show all\" button and wait for $waitingTime ms for a new window")
+ mDevice
+ .findObject(
+ UiSelector()
+ .textContains(getStringResource(R.string.recent_tabs_show_all)),
+ ).clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickJumpBackInShowAllButton: Clicked \"Show all\" button and wait for $waitingTime ms for a new window")
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickJumpBackInShowAllButton(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickJumpBackInShowAllButton: Trying to click \"Show all\" button and wait for $waitingTime ms for a new window")
+ mDevice
+ .findObject(
+ UiSelector()
+ .textContains(getStringResource(R.string.recent_tabs_show_all)),
+ ).clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickJumpBackInShowAllButton: Clicked \"Show all\" button and wait for $waitingTime ms for a new window")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun clickPocketStoryItem(publisher: String, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickPocketStoryItem: Trying to click pocket story item published by: $publisher at position: $position and wait for $waitingTime ms for a new window")
+ mDevice.findObject(
+ UiSelector()
+ .className("android.view.View")
+ .index(position - 1),
+ ).getChild(
+ UiSelector()
+ .className("android.widget.TextView")
+ .index(1)
+ .textContains(publisher),
+ ).clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickPocketStoryItem: Clicked pocket story item published by: $publisher at position: $position and wait for $waitingTime ms for a new window")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickPocketDiscoverMoreButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickPocketDiscoverMoreButton: Trying to scroll into view the \"Discover more\" button")
+ pocketStoriesList()
+ .scrollIntoView(UiSelector().text("Discover more"))
+ Log.i(TAG, "clickPocketDiscoverMoreButton: Scrolled into view the \"Discover more\" button")
+
+ mDevice.findObject(UiSelector().text("Discover more")).also {
+ Log.i(TAG, "clickPocketDiscoverMoreButton: Waiting for $waitingTime ms for \"Discover more\" button to exist")
+ it.waitForExists(waitingTimeShort)
+ Log.i(TAG, "clickPocketDiscoverMoreButton: Waited for $waitingTime ms for \"Discover more\" button to exist")
+ Log.i(TAG, "clickPocketDiscoverMoreButton: Trying to click \"Discover more\" button")
+ it.click()
+ Log.i(TAG, "clickPocketDiscoverMoreButton: Clicked \"Discover more\" button")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickPocketLearnMoreLink(composeTestRule: ComposeTestRule, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickPocketLearnMoreLink: Trying to click pocket \"Learn more\" link")
+ composeTestRule.onNodeWithTag("pocket.header.subtitle", true).performClick()
+ Log.i(TAG, "clickPocketLearnMoreLink: Clicked pocket \"Learn more\" link")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickSetAsDefaultBrowserOnboardingButton(
+ composeTestRule: ComposeTestRule,
+ interact: SettingsRobot.() -> Unit,
+ ): SettingsRobot.Transition {
+ Log.i(TAG, "clickSetAsDefaultBrowserOnboardingButton: Trying to click \"Set as default browser\" onboarding button")
+ composeTestRule.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_default_browser_positive_button),
+ ).performClick()
+ Log.i(TAG, "clickSetAsDefaultBrowserOnboardingButton: Clicked \"Set as default browser\" onboarding button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun clickSignInOnboardingButton(
+ composeTestRule: ComposeTestRule,
+ interact: SyncSignInRobot.() -> Unit,
+ ): SyncSignInRobot.Transition {
+ Log.i(TAG, "clickSignInOnboardingButton: Trying to click \"Sign in\" onboarding button")
+ composeTestRule.onNodeWithText(
+ getStringResource(R.string.juno_onboarding_sign_in_positive_button),
+ ).performClick()
+ Log.i(TAG, "clickSignInOnboardingButton: Clicked \"Sign in\" onboarding button")
+
+ SyncSignInRobot().interact()
+ return SyncSignInRobot.Transition()
+ }
+ }
+}
+
+fun homeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+}
+
+fun homeScreenWithComposeTopSites(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTopSitesRobot.() -> Unit): ComposeTopSitesRobot.Transition {
+ ComposeTopSitesRobot(composeTestRule).interact()
+ return ComposeTopSitesRobot.Transition(composeTestRule)
+}
+
+private fun homeScreenList() =
+ UiScrollable(
+ UiSelector()
+ .resourceId("$packageName:id/sessionControlRecyclerView")
+ .scrollable(true),
+ ).setAsVerticalList()
+
+private fun threeDotButton() = onView(allOf(withId(R.id.menuButton)))
+
+private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button))
+
+private fun tabsCounter() = onView(withId(R.id.tab_button))
+
+private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
+ onView(
+ allOf(
+ withId(R.id.top_site_title),
+ withText(sponsoredShortcutTitle),
+ ),
+ )
+
+private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
+ composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
+
+private fun homeScreen() =
+ itemWithResId("$packageName:id/homeLayout")
+private fun privateBrowsingButton() =
+ itemWithResId("$packageName:id/privateBrowsingButton")
+
+private fun isPrivateModeEnabled(): Boolean =
+ itemWithResIdAndDescription(
+ "$packageName:id/privateBrowsingButton",
+ "Disable private browsing",
+ ).exists()
+
+private fun homepageWordmark() =
+ itemWithResId("$packageName:id/wordmark")
+
+private fun navigationToolbar() =
+ itemWithResId("$packageName:id/toolbar")
+private fun menuButton() =
+ itemWithResId("$packageName:id/menuButton")
+private fun tabCounter(numberOfOpenTabs: String) =
+ itemWithResIdAndText("$packageName:id/counter_text", numberOfOpenTabs)
+
+fun deleteFromHistory() =
+ onView(
+ allOf(
+ withId(R.id.simple_text),
+ withText(R.string.delete_from_history),
+ ),
+ ).inRoot(RootMatchers.isPlatformPopup())
+
+private fun sponsoredShortcutsSettingsButton() =
+ mDevice
+ .findObject(
+ UiSelector()
+ .textContains(getStringResource(R.string.top_sites_menu_settings))
+ .resourceId("$packageName:id/simple_text"),
+ )
+
+private fun sponsorsAndPrivacyButton() =
+ mDevice
+ .findObject(
+ UiSelector()
+ .textContains(getStringResource(R.string.top_sites_menu_sponsor_privacy))
+ .resourceId("$packageName:id/simple_text"),
+ )
+
+private fun pocketStoriesList() =
+ UiScrollable(UiSelector().resourceId("pocket.stories")).setAsHorizontalList()
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/LibrarySubMenusMultipleSelectionToolbarRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/LibrarySubMenusMultipleSelectionToolbarRobot.kt
new file mode 100644
index 0000000000..b3b3d395c8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/LibrarySubMenusMultipleSelectionToolbarRobot.kt
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.net.Uri
+import android.util.Log
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withChild
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import org.hamcrest.Matchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.tabstray.TabsTrayTestTag
+
+/*
+ * Implementation of Robot Pattern for the multiple selection toolbar of History and Bookmarks menus.
+ */
+class LibrarySubMenusMultipleSelectionToolbarRobot {
+
+ fun verifyMultiSelectionCheckmark() {
+ Log.i(TAG, "verifyMultiSelectionCheckmark: Trying to verify that the multi-selection checkmark is displayed")
+ onView(withId(R.id.checkmark)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyMultiSelectionCheckmark: Verified that the multi-selection checkmark is displayed")
+ }
+
+ fun verifyMultiSelectionCheckmark(url: Uri) {
+ Log.i(TAG, "verifyMultiSelectionCheckmark: Trying to verify that the multi-selection checkmark for item with url: $url is displayed")
+ onView(
+ allOf(
+ withId(R.id.checkmark),
+ withParent(
+ withParent(
+ withChild(
+ allOf(
+ withId(R.id.url),
+ withText(url.toString()),
+ ),
+ ),
+ ),
+ ),
+
+ // This is used as part of the `multiSelectionToolbarItemsTest` test. Somehow, in the view hierarchy,
+ // the match above is finding two checkmark views - one visible, one hidden, which is throwing off
+ // the matcher. This 'isDisplayed' check is a hacky workaround for this, we're explicitly ignoring
+ // the hidden one. Why are there two to begin with, though?
+ isDisplayed(),
+ ),
+ ).check(matches(isDisplayed()))
+ Log.i(Constants.TAG, "verifyMultiSelectionCheckmark: Verified that the multi-selection checkmark for item with url: $url is displayed")
+ }
+
+ fun verifyMultiSelectionCounter() {
+ Log.i(TAG, "verifyMultiSelectionCounter: Trying to verify that the multi-selection toolbar containing: \"1 selected\" is displayed")
+ onView(withText("1 selected")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyMultiSelectionCounter: Verified that the multi-selection toolbar containing: \"1 selected\" is displayed")
+ }
+
+ fun verifyShareHistoryButton() {
+ Log.i(TAG, "verifyShareHistoryButton: Trying to verify that the multi-selection share history button is displayed")
+ shareHistoryButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareHistoryButton: Verified that the multi-selection share history button is displayed")
+ }
+
+ fun verifyShareBookmarksButton() {
+ Log.i(TAG, "verifyShareBookmarksButton: Trying to verify that the multi-selection share bookmarks button is displayed")
+ shareBookmarksButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareBookmarksButton: Verified that the multi-selection share bookmarks button is displayed")
+ }
+
+ fun verifyShareOverlay() {
+ Log.i(TAG, "verifyShareOverlay: Trying to verify that the share overlay is displayed")
+ onView(withId(R.id.shareWrapper)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareOverlay: Verified that the share overlay is displayed")
+ }
+
+ fun verifyShareTabFavicon() {
+ Log.i(TAG, "verifyShareTabFavicon: Trying to verify that the shared tab favicon is displayed")
+ onView(withId(R.id.share_tab_favicon)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabFavicon: Verified that the shared tab favicon is displayed")
+ }
+
+ fun verifyShareTabTitle() {
+ Log.i(TAG, "verifyShareTabTitle: Trying to verify that the shared tab title is displayed")
+ onView(withId(R.id.share_tab_title)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabTitle: Verified that the shared tab title is displayed")
+ }
+
+ fun verifyShareTabUrl() {
+ Log.i(TAG, "verifyShareTabUrl: Trying to verify that the shared tab url is displayed")
+ onView(withId(R.id.share_tab_url)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabUrl: Verified that the shared tab url is displayed")
+ }
+
+ fun verifyCloseToolbarButton() {
+ Log.i(TAG, "verifyCloseToolbarButton: Trying to verify that the navigate up toolbar button is displayed")
+ closeToolbarButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCloseToolbarButton: Verified that the navigate up toolbar button is displayed")
+ }
+
+ fun clickShareHistoryButton() {
+ Log.i(TAG, "clickShareHistoryButton: Trying to click the multi-selection share history button")
+ shareHistoryButton().click()
+ Log.i(TAG, "clickShareHistoryButton: Clicked the multi-selection share history button")
+
+ mDevice.waitNotNull(
+ Until.findObject(
+ By.text("ALL ACTIONS"),
+ ),
+ waitingTime,
+ )
+ }
+
+ fun clickShareBookmarksButton() {
+ Log.i(TAG, "clickShareBookmarksButton: Trying to click the multi-selection share bookmarks button")
+ shareBookmarksButton().click()
+ Log.i(TAG, "clickShareBookmarksButton: Clicked the multi-selection share bookmarks button")
+
+ mDevice.waitNotNull(
+ Until.findObject(
+ By.text("ALL ACTIONS"),
+ ),
+ waitingTime,
+ )
+ }
+
+ fun clickMultiSelectionDelete() {
+ Log.i(TAG, "clickMultiSelectionDelete: Trying to click the multi-selection delete button")
+ deleteButton().click()
+ Log.i(TAG, "clickMultiSelectionDelete: Clicked the multi-selection delete button")
+ }
+
+ class Transition {
+ fun closeToolbarReturnToHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
+ Log.i(TAG, "closeToolbarReturnToHistory: Trying to click the navigate up toolbar button")
+ closeToolbarButton().click()
+ Log.i(TAG, "closeToolbarReturnToHistory: Clicked the navigate up toolbar button")
+
+ HistoryRobot().interact()
+ return HistoryRobot.Transition()
+ }
+
+ fun closeToolbarReturnToBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ Log.i(TAG, "closeToolbarReturnToBookmarks: Trying to click the navigate up toolbar button")
+ closeToolbarButton().click()
+ Log.i(TAG, "closeToolbarReturnToBookmarks: Clicked the navigate up toolbar button")
+
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+ }
+
+ fun clickOpenNewTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenNewTab: Trying to click the multi-select \"Open in a new tab\" context menu button")
+ openInNewTabButton().click()
+ Log.i(TAG, "clickOpenNewTab: Clicked the multi-select \"Open in a new tab\" context menu button")
+
+ mDevice.waitNotNull(
+ Until.findObject(By.res("$packageName:id/tab_layout")),
+ waitingTime,
+ )
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickOpenNewTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenNewTab: Trying to click the multi-select \"Open in a new tab\" context menu button")
+ openInNewTabButton().click()
+ Log.i(TAG, "clickOpenNewTab: Clicked the multi-select \"Open in a new tab\" context menu button")
+ Log.i(TAG, "clickOpenNewTab: Trying to verify that the tabs tray exists")
+ composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
+ Log.i(TAG, "clickOpenNewTab: Verified that the tabs tray exists")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun clickOpenPrivateTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenPrivateTab: Trying to click the multi-select \"Open in a private tab\" context menu button")
+ openInPrivateTabButton().click()
+ Log.i(TAG, "clickOpenPrivateTab: Clicked the multi-select \"Open in a private tab\" context menu button")
+ mDevice.waitNotNull(
+ Until.findObject(By.res("$packageName:id/tab_layout")),
+ waitingTime,
+ )
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickOpenPrivateTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenPrivateTab: Trying to click the multi-select \"Open in a private tab\" context menu button")
+ openInPrivateTabButton().click()
+ Log.i(TAG, "clickOpenPrivateTab: Clicked the multi-select \"Open in a private tab\" context menu button")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+ }
+}
+
+fun multipleSelectionToolbar(interact: LibrarySubMenusMultipleSelectionToolbarRobot.() -> Unit): LibrarySubMenusMultipleSelectionToolbarRobot.Transition {
+ LibrarySubMenusMultipleSelectionToolbarRobot().interact()
+ return LibrarySubMenusMultipleSelectionToolbarRobot.Transition()
+}
+
+private fun closeToolbarButton() = onView(withContentDescription("Navigate up"))
+
+private fun shareHistoryButton() = onView(withId(R.id.share_history_multi_select))
+
+private fun shareBookmarksButton() = onView(withId(R.id.share_bookmark_multi_select))
+
+private fun openInNewTabButton() = onView(withText("Open in new tab"))
+
+private fun openInPrivateTabButton() = onView(withText("Open in private tab"))
+
+private fun deleteButton() = onView(withText("Delete"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt
new file mode 100644
index 0000000000..a2959d67aa
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt
@@ -0,0 +1,486 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.net.Uri
+import android.os.Build
+import android.util.Log
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.By.textContains
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.CoreMatchers.allOf
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.waitForObjects
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.tabstray.TabsTrayTestTag
+
+/**
+ * Implementation of Robot Pattern for the URL toolbar.
+ */
+class NavigationToolbarRobot {
+ fun verifyUrl(url: String) {
+ Log.i(TAG, "verifyUrl: Trying to verify toolbar text matches $url")
+ onView(withId(R.id.mozac_browser_toolbar_url_view)).check(matches(withText(url)))
+ Log.i(TAG, "verifyUrl: Verified toolbar text matches $url")
+ }
+
+ fun verifyTabButtonShortcutMenuItems() {
+ Log.i(TAG, "verifyTabButtonShortcutMenuItems: Trying to verify tab counter shortcut options")
+ onView(withId(R.id.mozac_browser_menu_recyclerView))
+ .check(matches(hasDescendant(withText("Close tab"))))
+ .check(matches(hasDescendant(withText("New private tab"))))
+ .check(matches(hasDescendant(withText("New tab"))))
+ Log.i(TAG, "verifyTabButtonShortcutMenuItems: Verified tab counter shortcut options")
+ }
+
+ fun verifyReaderViewDetected(visible: Boolean = false) {
+ Log.i(TAG, "verifyReaderViewDetected: Waiting for $waitingTime ms for reader view button to exist")
+ mDevice.findObject(
+ UiSelector()
+ .description("Reader view"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifyReaderViewDetected: Waited for $waitingTime ms for reader view button to exist")
+ Log.i(TAG, "verifyReaderViewDetected: Trying to verify that the reader view button is visible")
+ onView(
+ allOf(
+ withParent(withId(R.id.mozac_browser_toolbar_page_actions)),
+ withContentDescription("Reader view"),
+ ),
+ ).check(
+ if (visible) {
+ matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
+ } else {
+ ViewAssertions.doesNotExist()
+ },
+ )
+ Log.i(TAG, "verifyReaderViewDetected: Verified that the reader view button is visible")
+ }
+
+ fun toggleReaderView() {
+ Log.i(TAG, "toggleReaderView: Waiting for $waitingTime ms for reader view button to exist")
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/mozac_browser_toolbar_page_actions"),
+ )
+ .waitForExists(waitingTime)
+ Log.i(TAG, "toggleReaderView: Waited for $waitingTime ms for reader view button to exist")
+ Log.i(TAG, "toggleReaderView: Trying to click the reader view button")
+ readerViewToggle().click()
+ Log.i(TAG, "toggleReaderView: Clicked the reader view button")
+ }
+
+ fun verifyClipboardSuggestionsAreDisplayed(link: String = "", shouldBeDisplayed: Boolean) =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/fill_link_from_clipboard"),
+ itemWithResIdAndText(
+ "$packageName:id/clipboard_url",
+ link,
+ ),
+ exists = shouldBeDisplayed,
+ )
+
+ fun longClickEditModeToolbar() {
+ Log.i(TAG, "longClickEditModeToolbar: Trying to long click the edit mode toolbar")
+ mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view"))
+ .click(LONG_CLICK_DURATION)
+ Log.i(TAG, "longClickEditModeToolbar: Long clicked the edit mode toolbar")
+ }
+
+ fun clickContextMenuItem(item: String) {
+ mDevice.waitNotNull(
+ Until.findObject(By.text(item)),
+ waitingTime,
+ )
+ Log.i(TAG, "clickContextMenuItem: Trying click context menu item: $item")
+ mDevice.findObject(By.text(item)).click()
+ Log.i(TAG, "clickContextMenuItem: Clicked context menu item: $item")
+ }
+
+ fun clickClearToolbarButton() {
+ Log.i(TAG, "clickClearToolbarButton: Trying click the clear address button")
+ clearAddressBarButton().click()
+ Log.i(TAG, "clickClearToolbarButton: Clicked the clear address button")
+ }
+
+ fun verifyToolbarIsEmpty() =
+ assertUIObjectExists(
+ itemWithResIdContainingText(
+ "$packageName:id/mozac_browser_toolbar_edit_url_view",
+ getStringResource(R.string.search_hint),
+ ),
+ )
+
+ // New unified search UI selector
+ fun verifySearchBarPlaceholder(text: String) {
+ Log.i(TAG, "verifySearchBarPlaceholder: Waiting for $waitingTime ms for the toolbar to exist")
+ urlBar().waitForExists(waitingTime)
+ Log.i(TAG, "verifySearchBarPlaceholder: Waited for $waitingTime ms for the toolbar to exist")
+ assertItemTextEquals(urlBar(), expectedText = text)
+ }
+
+ // New unified search UI selector
+ fun verifyDefaultSearchEngine(engineName: String) =
+ assertUIObjectExists(
+ searchSelectorButton().getChild(UiSelector().description(engineName)),
+ )
+
+ fun verifyTextSelectionOptions(vararg textSelectionOptions: String) {
+ for (textSelectionOption in textSelectionOptions) {
+ mDevice.waitNotNull(Until.findObject(textContains(textSelectionOption)), waitingTime)
+ }
+ }
+
+ class Transition {
+ private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
+
+ fun enterURLAndEnterToBrowser(
+ url: Uri,
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ sessionLoadedIdlingResource = SessionLoadedIdlingResource()
+
+ openEditURLView()
+ Log.i(TAG, "enterURLAndEnterToBrowser: Trying to set toolbar text to: $url")
+ awesomeBar().setText(url.toString())
+ Log.i(TAG, "enterURLAndEnterToBrowser: Toolbar text was set to: $url")
+ Log.i(TAG, "enterURLAndEnterToBrowser: Trying to press device enter button")
+ mDevice.pressEnter()
+ Log.i(TAG, "enterURLAndEnterToBrowser: Pressed device enter button")
+
+ registerAndCleanupIdlingResources(sessionLoadedIdlingResource) {
+ Log.i(TAG, "enterURLAndEnterToBrowser: Trying to assert that home screen layout or download button or the total cookie protection contextual hint exist")
+ assertTrue(
+ itemWithResId("$packageName:id/browserLayout").waitForExists(waitingTime) ||
+ itemWithResId("$packageName:id/download_button").waitForExists(waitingTime) ||
+ itemWithResId("cfr.dismiss").waitForExists(waitingTime),
+ )
+ Log.i(TAG, "enterURLAndEnterToBrowser: Asserted that home screen layout or download button or the total cookie protection contextual hint exist")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun enterURLAndEnterToBrowserForTCPCFR(
+ url: Uri,
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ openEditURLView()
+ Log.i(TAG, "enterURLAndEnterToBrowserForTCPCFR: Trying to set toolbar text to: $url")
+ awesomeBar().setText(url.toString())
+ Log.i(TAG, "enterURLAndEnterToBrowserForTCPCFR: Toolbar text was set to: $url")
+ Log.i(TAG, "enterURLAndEnterToBrowserForTCPCFR: Trying to press device enter button")
+ mDevice.pressEnter()
+ Log.i(TAG, "enterURLAndEnterToBrowserForTCPCFR: Pressed device enter button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openTabCrashReporter(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ val crashUrl = "about:crashcontent"
+
+ sessionLoadedIdlingResource = SessionLoadedIdlingResource()
+
+ openEditURLView()
+ Log.i(TAG, "openTabCrashReporter: Trying to set toolbar text to: $crashUrl")
+ awesomeBar().setText(crashUrl)
+ Log.i(TAG, "openTabCrashReporter: Toolbar text was set to: $crashUrl")
+ Log.i(TAG, "openTabCrashReporter: Trying to press device enter button")
+ mDevice.pressEnter()
+ Log.i(TAG, "openTabCrashReporter: Pressed device enter button")
+
+ registerAndCleanupIdlingResources(sessionLoadedIdlingResource) {
+ Log.i(TAG, "openTabCrashReporter: Trying to find the tab crasher image")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/crash_tab_image"))
+ Log.i(TAG, "openTabCrashReporter: Found the tab crasher image")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
+ mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_menu")), waitingTime)
+ Log.i(TAG, "openThreeDotMenu: Trying to click the main menu button")
+ threeDotButton().click()
+ Log.i(TAG, "openThreeDotMenu: Clicked the main menu button")
+
+ ThreeDotMenuMainRobot().interact()
+ return ThreeDotMenuMainRobot.Transition()
+ }
+
+ fun openTabTray(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "openTabTray: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "openTabTray: Waited for device to be idle for $waitingTime ms")
+ Log.i(TAG, "openTabTray: Trying to click the tabs tray button")
+ tabTrayButton().click()
+ Log.i(TAG, "openTabTray: Clicked the tabs tray button")
+ mDevice.waitNotNull(
+ Until.findObject(By.res("$packageName:id/tab_layout")),
+ waitingTime,
+ )
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun openComposeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ for (i in 1..Constants.RETRY_COUNT) {
+ try {
+ Log.i(TAG, "openComposeTabDrawer: Started try #$i")
+ mDevice.waitForObjects(
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/mozac_browser_toolbar_browser_actions"),
+ ),
+ waitingTime,
+ )
+ Log.i(TAG, "openComposeTabDrawer: Trying to click the tabs tray button")
+ tabTrayButton().click()
+ Log.i(TAG, "openComposeTabDrawer: Clicked the tabs tray button")
+ Log.i(TAG, "openComposeTabDrawer: Trying to verify that the tabs tray exists")
+ composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()
+ Log.i(TAG, "openComposeTabDrawer: Verified that the tabs tray exists")
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "openComposeTabDrawer: AssertionError caught, executing fallback methods")
+ if (i == Constants.RETRY_COUNT) {
+ throw e
+ } else {
+ Log.i(TAG, "openComposeTabDrawer: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "openComposeTabDrawer: Waited for device to be idle")
+ }
+ }
+ }
+ Log.i(TAG, "openComposeTabDrawer: Trying to verify the tabs tray new tab FAB button exists")
+ composeTestRule.onNodeWithTag(TabsTrayTestTag.fab).assertExists()
+ Log.i(TAG, "openComposeTabDrawer: Verified the tabs tray new tab FAB button exists")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun visitLinkFromClipboard(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "visitLinkFromClipboard: Waiting for $waitingTimeShort ms for clear address button to exist")
+ if (clearAddressBarButton().waitForExists(waitingTimeShort)) {
+ Log.i(TAG, "visitLinkFromClipboard: Waited for $waitingTimeShort ms for clear address button to exist")
+ Log.i(TAG, "visitLinkFromClipboard: Trying to click the clear address button")
+ clearAddressBarButton().click()
+ Log.i(TAG, "visitLinkFromClipboard: Clicked the clear address button")
+ }
+
+ mDevice.waitNotNull(
+ Until.findObject(By.res("$packageName:id/clipboard_title")),
+ waitingTime,
+ )
+
+ // On Android 12 or above we don't SHOW the URL unless the user requests to do so.
+ // See for mor information https://github.com/mozilla-mobile/fenix/issues/22271
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ mDevice.waitNotNull(
+ Until.findObject(By.res("$packageName:id/clipboard_url")),
+ waitingTime,
+ )
+ }
+ Log.i(TAG, "visitLinkFromClipboard: Trying to click the fill link from clipboard button")
+ fillLinkButton().click()
+ Log.i(TAG, "visitLinkFromClipboard: Clicked the fill link from clipboard button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun goBackToHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBackToHomeScreen: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBackToHomeScreen: Clicked the device back button")
+ Log.i(TAG, "goBackToHomeScreen: Waiting for $waitingTimeShort ms for $packageName window to be updated")
+ mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
+ Log.i(TAG, "goBackToHomeScreen: Waited for $waitingTimeShort ms for $packageName window to be updated")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goBackToBrowserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goBackToBrowserScreen: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBackToBrowserScreen: Clicked the device back button")
+ Log.i(TAG, "goBackToBrowserScreen: Waiting for $waitingTimeShort ms for $packageName window to be updated")
+ mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
+ Log.i(TAG, "goBackToBrowserScreen: Waited for $waitingTimeShort ms for $packageName window to be updated")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): Transition {
+ mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/counter_root")))
+ Log.i(TAG, "openTabButtonShortcutsMenu: Trying to long click the tab counter button")
+ tabsCounter().perform(longClick())
+ Log.i(TAG, "openTabButtonShortcutsMenu: Long clicked the tab counter button")
+
+ NavigationToolbarRobot().interact()
+ return Transition()
+ }
+
+ fun closeTabFromShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): Transition {
+ Log.i(TAG, "closeTabFromShortcutsMenu: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "closeTabFromShortcutsMenu: Waited for device to be idle for $waitingTime ms")
+ Log.i(TAG, "closeTabFromShortcutsMenu: Trying to click the \"Close tab\" button")
+ onView(withId(R.id.mozac_browser_menu_recyclerView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(
+ withText("Close tab"),
+ ),
+ ViewActions.click(),
+ ),
+ )
+ Log.i(TAG, "closeTabFromShortcutsMenu: Clicked the \"Close tab\" button")
+
+ NavigationToolbarRobot().interact()
+ return Transition()
+ }
+
+ fun openNewTabFromShortcutsMenu(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "openNewTabFromShortcutsMenu: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "openNewTabFromShortcutsMenu: Waited for device to be idle for $waitingTime ms")
+ Log.i(TAG, "openNewTabFromShortcutsMenu: Trying to click the \"New tab\" button")
+ onView(withId(R.id.mozac_browser_menu_recyclerView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(
+ withText("New tab"),
+ ),
+ ViewActions.click(),
+ ),
+ )
+ Log.i(TAG, "openNewTabFromShortcutsMenu: Clicked the \"New tab\" button")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+
+ fun openNewPrivateTabFromShortcutsMenu(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "openNewPrivateTabFromShortcutsMenu: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "openNewPrivateTabFromShortcutsMenu: Waited for device to be idle for $waitingTime ms")
+ Log.i(TAG, "openNewPrivateTabFromShortcutsMenu: Trying to click the \"New private tab\" button")
+ onView(withId(R.id.mozac_browser_menu_recyclerView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(
+ withText("New private tab"),
+ ),
+ ViewActions.click(),
+ ),
+ )
+ Log.i(TAG, "openNewPrivateTabFromShortcutsMenu: Clicked the \"New private tab\" button")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+
+ fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "clickUrlbar: Trying to click the toolbar")
+ urlBar().click()
+ Log.i(TAG, "clickUrlbar: Clicked the toolbar")
+ Log.i(TAG, "clickUrlbar: Waiting for $waitingTime ms for the edit mode toolbar to exist")
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "clickUrlbar: Waited for $waitingTime ms for the edit mode toolbar to exist")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+
+ fun clickSearchSelectorButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "clickSearchSelectorButton: Waiting for $waitingTime ms for the search selector button to exist")
+ searchSelectorButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickSearchSelectorButton: Waited for $waitingTime ms for the search selector button to exist")
+ Log.i(TAG, "clickSearchSelectorButton: Trying to click the search selector button")
+ searchSelectorButton().click()
+ Log.i(TAG, "clickSearchSelectorButton: Clicked the search selector button")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+ }
+}
+
+fun navigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
+ NavigationToolbarRobot().interact()
+ return NavigationToolbarRobot.Transition()
+}
+
+fun openEditURLView() {
+ Log.i(TAG, "openEditURLView: Waiting for $waitingTime ms for the toolbar to exist")
+ urlBar().waitForExists(waitingTime)
+ Log.i(TAG, "openEditURLView: Waited for $waitingTime ms for the toolbar to exist")
+ Log.i(TAG, "openEditURLView: Trying to click the toolbar")
+ urlBar().click()
+ Log.i(TAG, "openEditURLView: Clicked the toolbar")
+ Log.i(TAG, "openEditURLView: Waiting for $waitingTime ms for the edit mode toolbar to exist")
+ itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view").waitForExists(waitingTime)
+ Log.i(TAG, "openEditURLView: Waited for $waitingTime ms for the edit mode toolbar to exist")
+}
+
+private fun urlBar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
+private fun awesomeBar() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
+private fun threeDotButton() = onView(withId(R.id.mozac_browser_toolbar_menu))
+private fun tabTrayButton() = onView(withId(R.id.tab_button))
+private fun tabsCounter() = onView(withId(R.id.mozac_browser_toolbar_browser_actions))
+private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
+private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view")
+private fun readerViewToggle() =
+ onView(withParent(withId(R.id.mozac_browser_toolbar_page_actions)))
+
+private fun searchSelectorButton() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/search_selector"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt
new file mode 100644
index 0000000000..99cbbf9a69
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt
@@ -0,0 +1,296 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.app.NotificationManager
+import android.content.Context
+import android.util.Log
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import kotlin.AssertionError
+
+class NotificationRobot {
+
+ fun verifySystemNotificationExists(notificationMessage: String) {
+ val notification = UiSelector().text(notificationMessage)
+ var notificationFound = mDevice.findObject(notification).waitForExists(waitingTime)
+
+ while (!notificationFound) {
+ Log.i(TAG, "verifySystemNotificationExists: Waiting for $waitingTime ms for notification: $notification to exist")
+ scrollToEnd()
+ notificationFound = mDevice.findObject(notification).waitForExists(waitingTime)
+ Log.i(TAG, "verifySystemNotificationExists: Waited for $waitingTime ms for notification: $notification to exist")
+ }
+
+ assertUIObjectExists(itemWithText(notificationMessage))
+ }
+
+ fun clearNotifications() {
+ if (clearButton().exists()) {
+ Log.i(TAG, "clearNotifications:The clear notifications button exists")
+ Log.i(TAG, "clearNotifications: Trying to click the clear notifications button")
+ clearButton().click()
+ Log.i(TAG, "clearNotifications: Clicked the clear notifications button")
+ } else {
+ scrollToEnd()
+ if (clearButton().exists()) {
+ Log.i(TAG, "clearNotifications:The clear notifications button exists")
+ Log.i(TAG, "clearNotifications: Trying to click the clear notifications button")
+ clearButton().click()
+ Log.i(TAG, "clearNotifications: Clicked the clear notifications button")
+ } else if (notificationTray().exists()) {
+ Log.i(TAG, "clearNotifications: The notifications tray is still displayed")
+ Log.i(TAG, "clearNotifications: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "clearNotifications: Clicked device back button")
+ }
+ }
+ }
+
+ fun cancelAllShownNotifications() {
+ Log.i(TAG, "cancelAllShownNotifications: Trying to cancel all system notifications")
+ cancelAll()
+ Log.i(TAG, "cancelAllShownNotifications: Canceled all system notifications")
+ }
+
+ fun verifySystemNotificationDoesNotExist(notificationMessage: String) {
+ Log.i(TAG, "verifySystemNotificationDoesNotExist: Waiting for $waitingTime ms for notification: $notificationMessage to be gone")
+ mDevice.findObject(UiSelector().textContains(notificationMessage)).waitUntilGone(waitingTime)
+ Log.i(TAG, "verifySystemNotificationDoesNotExist: Waited for $waitingTime ms for notification: $notificationMessage to be gone")
+ assertUIObjectExists(itemContainingText(notificationMessage), exists = false)
+ }
+
+ fun verifyPrivateTabsNotification() {
+ verifySystemNotificationExists("$appName (Private)")
+ verifySystemNotificationExists("Close private tabs")
+ }
+
+ fun clickMediaNotificationControlButton(action: String) {
+ Log.i(TAG, "clickMediaNotificationControlButton: Waiting for $waitingTime ms for the system media control button: $action to exist")
+ mediaSystemNotificationButton(action).waitForExists(waitingTime)
+ Log.i(TAG, "clickMediaNotificationControlButton: Waited for $waitingTime ms for the system media control button: $action to exist")
+ Log.i(TAG, "clickMediaNotificationControlButton: Trying to click the system media control button: $action")
+ mediaSystemNotificationButton(action).click()
+ Log.i(TAG, "clickMediaNotificationControlButton: Clicked the system media control button: $action")
+ }
+
+ fun clickDownloadNotificationControlButton(action: String) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "clickDownloadNotificationControlButton: Started try #$i")
+ try {
+ assertUIObjectExists(downloadSystemNotificationButton(action))
+ Log.i(TAG, "clickDownloadNotificationControlButton: Trying to click the download system notification: $action button and wait for $waitingTimeShort ms for a new window")
+ downloadSystemNotificationButton(action).clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "clickDownloadNotificationControlButton: Clicked the download system notification: $action button and waited for $waitingTimeShort ms for a new window")
+ assertUIObjectExists(
+ downloadSystemNotificationButton(action),
+ exists = false,
+ )
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "clickDownloadNotificationControlButton: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ }
+ Log.i(TAG, "clickDownloadNotificationControlButton: Waiting for $waitingTimeShort ms for $packageName window to be updated")
+ mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
+ Log.i(TAG, "clickDownloadNotificationControlButton: Waited for $waitingTimeShort ms for $packageName window to be updated")
+ }
+ }
+ }
+
+ fun verifyMediaSystemNotificationButtonState(action: String) =
+ assertUIObjectExists(mediaSystemNotificationButton(action))
+
+ fun expandNotificationMessage() {
+ while (!notificationHeader().exists()) {
+ Log.i(TAG, "expandNotificationMessage: Waiting for $appName notification to exist")
+ scrollToEnd()
+ }
+
+ if (notificationHeader().exists()) {
+ Log.i(TAG, "expandNotificationMessage: $appName notification exists")
+ // expand the notification
+ Log.i(TAG, "expandNotificationMessage: Trying to click $appName notification")
+ notificationHeader().click()
+ Log.i(TAG, "expandNotificationMessage: Clicked $appName notification")
+
+ // double check if notification actions are viewable by checking for action existence; otherwise scroll again
+ while (!mDevice.findObject(UiSelector().resourceId("android:id/action0")).exists() &&
+ !mDevice.findObject(UiSelector().resourceId("android:id/actions_container")).exists()
+ ) {
+ Log.i(TAG, "expandNotificationMessage: App notification action buttons do not exist")
+ scrollToEnd()
+ }
+ }
+ }
+
+ // Performs swipe action on download system notifications
+ fun swipeDownloadNotification(
+ direction: String,
+ shouldDismissNotification: Boolean,
+ canExpandNotification: Boolean = true,
+ ) {
+ // In case it fails, retry max 3x the swipe action on download system notifications
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "swipeDownloadNotification: Started try #$i")
+ try {
+ var retries = 0
+ while (itemContainingText(appName).exists() && retries++ < 3) {
+ // Swipe left the download system notification
+ if (direction == "Left") {
+ itemContainingText(appName)
+ .also {
+ Log.i(TAG, "swipeDownloadNotification: Waiting for $waitingTime ms for $appName notification to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "swipeDownloadNotification: Waited for $waitingTime ms for $appName notification to exist")
+ Log.i(TAG, "swipeDownloadNotification: Trying to perform swipe left action on $appName notification")
+ it.swipeLeft(3)
+ Log.i(TAG, "swipeDownloadNotification: Performed swipe left action on $appName notification")
+ }
+ } else {
+ // Swipe right the download system notification
+ itemContainingText(appName)
+ .also {
+ Log.i(TAG, "swipeDownloadNotification: Waiting for $waitingTime ms for $appName notification to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "swipeDownloadNotification: Waited for $waitingTime ms for $appName notification to exist")
+ Log.i(TAG, "swipeDownloadNotification: Trying to perform swipe right action on $appName notification")
+ it.swipeRight(3)
+ Log.i(TAG, "swipeDownloadNotification: Performed swipe right action on $appName notification")
+ }
+ }
+ }
+ // Not all download related system notifications can be dismissed
+ if (shouldDismissNotification) {
+ Log.i(TAG, "swipeDownloadNotification: $appName notification can't be dismissed: $shouldDismissNotification")
+ assertUIObjectExists(itemContainingText(appName), exists = false)
+ } else {
+ Log.i(TAG, "swipeDownloadNotification: $appName notification can be dismissed: $shouldDismissNotification")
+ assertUIObjectExists(itemContainingText(appName))
+ }
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "swipeDownloadNotification: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ notificationShade {
+ }.closeNotificationTray {
+ }.openNotificationShade {
+ // The download complete system notification can't be expanded
+ if (canExpandNotification) {
+ Log.i(TAG, "swipeDownloadNotification: $appName notification can be expanded: $canExpandNotification")
+ // In all cases the download system notification title will be the app name
+ verifySystemNotificationExists(appName)
+ expandNotificationMessage()
+ } else {
+ Log.i(TAG, "swipeDownloadNotification: $appName notification can't be expanded: $canExpandNotification")
+ // Using the download completed system notification summary to bring in to view an properly verify it
+ verifySystemNotificationExists("Download completed")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun clickNotification(notificationMessage: String) {
+ Log.i(TAG, "clickNotification: Waiting for $waitingTime ms for $notificationMessage notification to exist")
+ mDevice.findObject(UiSelector().text(notificationMessage)).waitForExists(waitingTime)
+ Log.i(TAG, "clickNotification: Waited for $waitingTime ms for $notificationMessage notification to exist")
+ Log.i(TAG, "clickNotification: Trying to click the $notificationMessage notification and wait for $waitingTimeShort ms for a new window")
+ mDevice.findObject(UiSelector().text(notificationMessage)).clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "clickNotification: Clicked the $notificationMessage notification and waited for $waitingTimeShort ms for a new window")
+ }
+
+ class Transition {
+
+ fun clickClosePrivateTabsNotification(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ try {
+ assertUIObjectExists(closePrivateTabsNotification())
+ } catch (e: AssertionError) {
+ Log.i(TAG, "clickClosePrivateTabsNotification: Trying to perform fling action to the end of the notification tray")
+ notificationTray().flingToEnd(1)
+ Log.i(TAG, "clickClosePrivateTabsNotification: Performed fling action to the end of the notification tray")
+ }
+ Log.i(TAG, "clickClosePrivateTabsNotification: Trying to click the close private tabs notification")
+ closePrivateTabsNotification().click()
+ Log.i(TAG, "clickClosePrivateTabsNotification: Clicked the close private tabs notification")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun closeNotificationTray(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeNotificationTray: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "closeNotificationTray: Clicked device back button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+fun notificationShade(interact: NotificationRobot.() -> Unit): NotificationRobot.Transition {
+ NotificationRobot().interact()
+ return NotificationRobot.Transition()
+}
+
+private fun closePrivateTabsNotification() =
+ mDevice.findObject(UiSelector().text("Close private tabs"))
+
+private fun downloadSystemNotificationButton(action: String) =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("android:id/action0")
+ .textContains(action),
+ )
+
+private fun mediaSystemNotificationButton(action: String) =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("com.android.systemui:id/action0")
+ .descriptionContains(action),
+ )
+
+private fun notificationTray() = UiScrollable(
+ UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller"),
+).setAsVerticalList()
+
+private fun notificationHeader() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("android:id/app_name_text")
+ .text(appName),
+ )
+
+private fun scrollToEnd() {
+ Log.i(TAG, "scrollToEnd: Trying to perform scroll to the end of the notification tray action")
+ notificationTray().scrollToEnd(1)
+ Log.i(TAG, "scrollToEnd: Performed scroll to the end of the notification tray action")
+}
+
+private fun clearButton() = mDevice.findObject(UiSelector().resourceId("com.android.systemui:id/dismiss_text"))
+
+private fun cancelAll() {
+ val notificationManager: NotificationManager =
+ TestHelper.appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancelAll()
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/PwaRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/PwaRobot.kt
new file mode 100644
index 0000000000..6456eadccf
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/PwaRobot.kt
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.uiautomator.UiSelector
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.helpers.AppAndSystemHelper.isExternalAppBrowserActivityInCurrentTask
+import org.mozilla.fenix.helpers.Constants
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+
+class PwaRobot {
+ fun verifyCustomTabToolbarIsNotDisplayed() = assertUIObjectExists(itemWithResId("$packageName:id/toolbar"), exists = false)
+ fun verifyPwaActivityInCurrentTask() {
+ assertTrue("$TAG: The latest activity of the application is not used for custom tabs or PWAs", isExternalAppBrowserActivityInCurrentTask())
+ Log.i(TAG, "verifyPwaActivityInCurrentTask: Verified that the latest activity of the application is used for custom tabs or PWAs")
+ }
+
+ class Transition
+}
+
+fun pwaScreen(interact: PwaRobot.() -> Unit): PwaRobot.Transition {
+ Log.i(TAG, "pwaScreen: Trying to find the engine view")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView"))
+ Log.i(Constants.TAG, "pwaScreen: Found the engine view")
+ PwaRobot().interact()
+ return PwaRobot.Transition()
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ReaderViewRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ReaderViewRobot.kt
new file mode 100644
index 0000000000..30d87d72c9
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ReaderViewRobot.kt
@@ -0,0 +1,267 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.content.Context
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for Reader View UI.
+ */
+class ReaderViewRobot {
+
+ fun verifyAppearanceFontGroup(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceFontGroup: Trying to verify that the font group buttons are visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_font_group),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceFontGroup: Verified that the font group buttons are visible: $visible")
+ }
+
+ fun verifyAppearanceFontSansSerif(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceFontSansSerif: Trying to verify that the sans serif font button is visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_font_sans_serif),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceFontSansSerif: Verified that the sans serif font button is visible: $visible")
+ }
+
+ fun verifyAppearanceFontSerif(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceFontSerif: Trying to verify that the serif font button is visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_font_serif),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceFontSerif: Verified that the serif font button is visible: $visible")
+ }
+
+ fun verifyAppearanceFontDecrease(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceFontDecrease: Trying to verify that the decrease font button is visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_font_size_decrease),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceFontDecrease: Verified that the decrease font button is visible: $visible")
+ }
+
+ fun verifyAppearanceFontIncrease(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceFontIncrease: Trying to verify that the increase font button is visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_font_size_increase),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceFontIncrease: Verified that the increase font button is visible: $visible")
+ }
+
+ fun verifyAppearanceColorGroup(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceColorGroup: Trying to verify that the color group buttons are visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_color_scheme_group),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceColorGroup: Verified that the color group buttons are visible: $visible")
+ }
+
+ fun verifyAppearanceColorSepia(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceColorSepia: Trying to verify that the sepia color button is visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_color_sepia),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceColorSepia: Verified that the sepia color button is visible: $visible")
+ }
+
+ fun verifyAppearanceColorDark(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceColorDark: Trying to verify that the dark color button is visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_color_dark),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceColorDark: Verified that the dark color button is visible: $visible")
+ }
+
+ fun verifyAppearanceColorLight(visible: Boolean = false) {
+ Log.i(TAG, "verifyAppearanceColorLight: Trying to verify that the light color button is visible: $visible")
+ onView(
+ withId(R.id.mozac_feature_readerview_color_light),
+ ).check(
+ matches(withEffectiveVisibility(visibleOrGone(visible))),
+ )
+ Log.i(TAG, "verifyAppearanceColorLight: Verified that the light color button is visible: $visible")
+ }
+
+ fun verifyAppearanceFontIsActive(fontType: String) {
+ Log.i(TAG, "verifyAppearanceFontIsActive: Trying to verify that the font type is: $fontType")
+ val fontTypeKey: String = "mozac-readerview-fonttype"
+
+ val prefs = InstrumentationRegistry.getInstrumentation()
+ .targetContext.getSharedPreferences(
+ "mozac_feature_reader_view",
+ Context.MODE_PRIVATE,
+ )
+
+ assertEquals(fontType, prefs.getString(fontTypeKey, ""))
+ Log.i(TAG, "verifyAppearanceFontIsActive: Verified that the font type is: $fontType")
+ }
+
+ fun verifyAppearanceFontSize(expectedFontSize: Int) {
+ Log.i(TAG, "verifyAppearanceFontSize: Trying to verify that the font size is: $expectedFontSize")
+ val fontSizeKey: String = "mozac-readerview-fontsize"
+
+ val prefs = InstrumentationRegistry.getInstrumentation()
+ .targetContext.getSharedPreferences(
+ "mozac_feature_reader_view",
+ Context.MODE_PRIVATE,
+ )
+
+ val fontSizeKeyValue = prefs.getInt(fontSizeKey, 3)
+
+ assertEquals(expectedFontSize, fontSizeKeyValue)
+ Log.i(TAG, "verifyAppearanceFontSize: Verified that the font size is: $expectedFontSize")
+ }
+
+ fun verifyAppearanceColorSchemeChange(expectedColorScheme: String) {
+ Log.i(TAG, "verifyAppearanceColorSchemeChange: Trying to verify that the color scheme is: $expectedColorScheme")
+ val colorSchemeKey: String = "mozac-readerview-colorscheme"
+
+ val prefs = InstrumentationRegistry.getInstrumentation()
+ .targetContext.getSharedPreferences(
+ "mozac_feature_reader_view",
+ Context.MODE_PRIVATE,
+ )
+
+ assertEquals(expectedColorScheme, prefs.getString(colorSchemeKey, ""))
+ Log.i(TAG, "verifyAppearanceColorSchemeChange: Verified that the color scheme is: $expectedColorScheme")
+ }
+
+ class Transition {
+
+ fun closeAppearanceMenu(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeAppearanceMenu: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "closeAppearanceMenu: Clicked device back button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun toggleSansSerif(interact: ReaderViewRobot.() -> Unit): Transition {
+ fun sansSerifButton() =
+ onView(
+ withId(R.id.mozac_feature_readerview_font_sans_serif),
+ )
+ Log.i(TAG, "toggleSansSerif: Trying to click sans serif button")
+ sansSerifButton().click()
+ Log.i(TAG, "toggleSansSerif: Clicked sans serif button")
+
+ ReaderViewRobot().interact()
+ return Transition()
+ }
+
+ fun toggleSerif(interact: ReaderViewRobot.() -> Unit): Transition {
+ fun serifButton() =
+ onView(
+ withId(R.id.mozac_feature_readerview_font_serif),
+ )
+ Log.i(TAG, "toggleSerif: Trying to click serif button")
+ serifButton().click()
+ Log.i(TAG, "toggleSerif: Clicked serif button")
+
+ ReaderViewRobot().interact()
+ return Transition()
+ }
+
+ fun toggleFontSizeDecrease(interact: ReaderViewRobot.() -> Unit): Transition {
+ fun fontSizeDecrease() =
+ onView(
+ withId(R.id.mozac_feature_readerview_font_size_decrease),
+ )
+ Log.i(TAG, "toggleFontSizeDecrease: Trying to click the decrease font button")
+ fontSizeDecrease().click()
+ Log.i(TAG, "toggleFontSizeDecrease: Clicked the decrease font button")
+
+ ReaderViewRobot().interact()
+ return Transition()
+ }
+
+ fun toggleFontSizeIncrease(interact: ReaderViewRobot.() -> Unit): Transition {
+ fun fontSizeIncrease() =
+ onView(
+ withId(R.id.mozac_feature_readerview_font_size_increase),
+ )
+ Log.i(TAG, "toggleFontSizeIncrease: Trying to click the increase font button")
+ fontSizeIncrease().click()
+ Log.i(TAG, "toggleFontSizeIncrease: Clicked the increase font button")
+
+ ReaderViewRobot().interact()
+ return Transition()
+ }
+
+ fun toggleColorSchemeChangeLight(interact: ReaderViewRobot.() -> Unit): Transition {
+ fun toggleLightColorSchemeButton() =
+ onView(
+ withId(R.id.mozac_feature_readerview_color_light),
+ )
+ Log.i(TAG, "toggleColorSchemeChangeLight: Trying to click the light color button")
+ toggleLightColorSchemeButton().click()
+ Log.i(TAG, "toggleColorSchemeChangeLight: Clicked the light color button")
+
+ ReaderViewRobot().interact()
+ return Transition()
+ }
+
+ fun toggleColorSchemeChangeDark(interact: ReaderViewRobot.() -> Unit): Transition {
+ fun toggleDarkColorSchemeButton() =
+ onView(
+ withId(R.id.mozac_feature_readerview_color_dark),
+ )
+ Log.i(TAG, "toggleColorSchemeChangeDark: Trying to click the dark color button")
+ toggleDarkColorSchemeButton().click()
+ Log.i(TAG, "toggleColorSchemeChangeDark: Clicked the dark color button")
+
+ ReaderViewRobot().interact()
+ return Transition()
+ }
+
+ fun toggleColorSchemeChangeSepia(interact: ReaderViewRobot.() -> Unit): Transition {
+ fun toggleSepiaColorSchemeButton() =
+ onView(
+ withId(R.id.mozac_feature_readerview_color_sepia),
+ )
+ Log.i(TAG, "toggleColorSchemeChangeSepia: Trying to click the sepia color button")
+ toggleSepiaColorSchemeButton().click()
+ Log.i(TAG, "toggleColorSchemeChangeSepia: Clicked the sepia color button")
+
+ ReaderViewRobot().interact()
+ return Transition()
+ }
+ }
+}
+
+private fun visibleOrGone(visibility: Boolean) =
+ if (visibility) ViewMatchers.Visibility.VISIBLE else ViewMatchers.Visibility.GONE
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt
new file mode 100644
index 0000000000..8034e46745
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.net.Uri
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.Matchers
+import org.hamcrest.Matchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the recently closed tabs menu.
+ */
+
+class RecentlyClosedTabsRobot {
+
+ fun waitForListToExist() {
+ Log.i(TAG, "waitForListToExist: Waiting for $waitingTime ms for recently closed tabs list to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/recently_closed_list"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "waitForListToExist: Waited for $waitingTime ms for recently closed tabs list to exist")
+ }
+
+ fun verifyRecentlyClosedTabsMenuView() {
+ Log.i(TAG, "verifyRecentlyClosedTabsMenuView: Trying to verify that the recently closed tabs menu view is visible")
+ onView(
+ allOf(
+ withText("Recently closed tabs"),
+ withParent(withId(R.id.navigationToolbar)),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyRecentlyClosedTabsMenuView: Verified that the recently closed tabs menu view is visible")
+ }
+
+ fun verifyEmptyRecentlyClosedTabsList() {
+ Log.i(TAG, "verifyEmptyRecentlyClosedTabsList: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "verifyEmptyRecentlyClosedTabsList: Waited for device to be idle")
+ Log.i(TAG, "verifyEmptyRecentlyClosedTabsList: Trying to verify that the empty recently closed tabs list is visible")
+ onView(
+ allOf(
+ withId(R.id.recently_closed_empty_view),
+ withText(R.string.recently_closed_empty_message),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEmptyRecentlyClosedTabsList: Verified that the empty recently closed tabs list is visible")
+ }
+
+ fun verifyRecentlyClosedTabsPageTitle(title: String) =
+ assertUIObjectExists(
+ recentlyClosedTabsPageTitle(title),
+ )
+
+ fun verifyRecentlyClosedTabsUrl(expectedUrl: Uri) {
+ Log.i(TAG, "verifyRecentlyClosedTabsUrl: Trying to verify that the recently closed tab with url: $expectedUrl is visible")
+ onView(
+ allOf(
+ withId(R.id.url),
+ withEffectiveVisibility(
+ Visibility.VISIBLE,
+ ),
+ ),
+ ).check(matches(withText(Matchers.containsString(expectedUrl.toString()))))
+ Log.i(TAG, "verifyRecentlyClosedTabsUrl: Verified that the recently closed tab with url: $expectedUrl is visible")
+ }
+
+ fun clickDeleteRecentlyClosedTabs() {
+ Log.i(TAG, "clickDeleteRecentlyClosedTabs: Trying to click the recently closed tab item delete button")
+ recentlyClosedTabDeleteButton().click()
+ Log.i(TAG, "clickDeleteRecentlyClosedTabs: Clicked the recently closed tab item delete button")
+ }
+
+ class Transition {
+ fun clickRecentlyClosedItem(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ recentlyClosedTabsPageTitle(title).also {
+ Log.i(TAG, "clickRecentlyClosedItem: Waiting for $waitingTimeShort ms for recently closed tab with title: $title to exist")
+ it.waitForExists(waitingTimeShort)
+ Log.i(TAG, "clickRecentlyClosedItem: Waited for $waitingTimeShort ms for recently closed tab with title: $title to exist")
+ Log.i(TAG, "clickRecentlyClosedItem: Trying to click the recently closed tab with title: $title")
+ it.click()
+ Log.i(TAG, "clickRecentlyClosedItem: Clicked the recently closed tab with title: $title")
+ }
+ Log.i(TAG, "clickRecentlyClosedItem: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "clickRecentlyClosedItem: Waited for device to be idle")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickOpenInNewTab(testRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenInNewTab: Trying to click the multi-select \"Open in a new tab\" context menu button")
+ openInNewTabOption().click()
+ Log.i(TAG, "clickOpenInNewTab: Clicked the multi-select \"Open in a new tab\" context menu button")
+
+ ComposeTabDrawerRobot(testRule).interact()
+ return ComposeTabDrawerRobot.Transition(testRule)
+ }
+
+ fun clickOpenInPrivateTab(testRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenInPrivateTab: Trying to click the multi-select \"Open in a private tab\" context menu button")
+ openInPrivateTabOption().click()
+ Log.i(TAG, "clickOpenInPrivateTab: Clicked the multi-select \"Open in a private tab\" context menu button")
+
+ ComposeTabDrawerRobot(testRule).interact()
+ return ComposeTabDrawerRobot.Transition(testRule)
+ }
+
+ fun clickShare(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ Log.i(TAG, "clickShare: Trying to click the share recently closed tabs button")
+ multipleSelectionShareButton().click()
+ Log.i(TAG, "clickShare: Clicked the share recently closed tabs button")
+
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+ }
+
+ fun goBackToHistoryMenu(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
+ Log.i(TAG, "goBackToHistoryMenu: Trying to click navigate up toolbar button")
+ onView(withContentDescription("Navigate up")).click()
+ Log.i(TAG, "goBackToHistoryMenu: Clicked navigate up toolbar button")
+
+ HistoryRobot().interact()
+ return HistoryRobot.Transition()
+ }
+ }
+}
+
+private fun recentlyClosedTabsPageTitle(title: String) =
+ itemWithResIdContainingText(
+ resourceId = "$packageName:id/title",
+ text = title,
+ )
+
+private fun recentlyClosedTabDeleteButton() =
+ onView(
+ allOf(
+ withId(R.id.overflow_menu),
+ withEffectiveVisibility(
+ Visibility.VISIBLE,
+ ),
+ ),
+ )
+
+private fun openInNewTabOption() = onView(withText("Open in new tab"))
+
+private fun openInPrivateTabOption() = onView(withText("Open in private tab"))
+
+private fun multipleSelectionShareButton() = onView(withId(R.id.share_history_multi_select))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt
new file mode 100644
index 0000000000..268517569d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt
@@ -0,0 +1,491 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.compose.ui.test.ComposeTimeoutException
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertAny
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performScrollToNode
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.PositionAssertions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers.allOf
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
+import org.mozilla.fenix.helpers.AppAndSystemHelper.isPackageInstalled
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
+import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_QUICK_SEARCH
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.SPEECH_RECOGNITION
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextContains
+import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.waitForObjects
+
+/**
+ * Implementation of Robot Pattern for the search fragment.
+ */
+class SearchRobot {
+ fun verifySearchView() = assertUIObjectExists(itemWithResId("$packageName:id/search_wrapper"))
+
+ fun verifySearchToolbar(isDisplayed: Boolean) =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view"),
+ exists = isDisplayed,
+ )
+
+ fun verifyScanButtonVisibility(visible: Boolean = true) =
+ assertUIObjectExists(scanButton(), exists = visible)
+
+ fun verifyVoiceSearchButtonVisibility(enabled: Boolean) =
+ assertUIObjectExists(voiceSearchButton(), exists = enabled)
+
+ // Device or AVD requires a Google Services Android OS installation
+ fun startVoiceSearch() {
+ Log.i(TAG, "startVoiceSearch: Trying to click the voice search button button")
+ voiceSearchButton().click()
+ Log.i(TAG, "startVoiceSearch: Clicked the voice search button button")
+ grantSystemPermission()
+
+ if (isPackageInstalled(GOOGLE_QUICK_SEARCH)) {
+ Log.i(TAG, "startVoiceSearch: $GOOGLE_QUICK_SEARCH is installed")
+ Log.i(TAG, "startVoiceSearch: Trying to verify the intent to: $GOOGLE_QUICK_SEARCH")
+ Intents.intended(IntentMatchers.hasAction(SPEECH_RECOGNITION))
+ Log.i(TAG, "startVoiceSearch: Verified the intent to: $GOOGLE_QUICK_SEARCH")
+ }
+ }
+
+ fun verifySearchEngineSuggestionResults(
+ rule: ComposeTestRule,
+ vararg searchSuggestions: String,
+ searchTerm: String,
+ shouldEditKeyword: Boolean = false,
+ numberOfDeletionSteps: Int = 0,
+ ) {
+ rule.waitForIdle()
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "verifySearchEngineSuggestionResults: Started try #$i")
+ try {
+ for (searchSuggestion in searchSuggestions) {
+ mDevice.waitForObjects(mDevice.findObject(UiSelector().textContains(searchSuggestion)))
+ Log.i(TAG, "verifySearchEngineSuggestionResults: Trying to perform scroll action to $searchSuggestion search suggestion")
+ rule.onNodeWithTag("mozac.awesomebar.suggestions").performScrollToNode(hasText(searchSuggestion))
+ Log.i(TAG, "verifySearchEngineSuggestionResults: Performed scroll action to $searchSuggestion search suggestion")
+ Log.i(TAG, "verifySearchEngineSuggestionResults: Trying to verify that $searchSuggestion search suggestion exists")
+ rule.onNodeWithTag("mozac.awesomebar.suggestions").assertExists()
+ Log.i(TAG, "verifySearchEngineSuggestionResults: Verified that $searchSuggestion search suggestion exists")
+ }
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifySearchEngineSuggestionResults: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ mDevice.pressBack()
+ homeScreen {
+ }.openSearch {
+ typeSearch(searchTerm)
+ if (shouldEditKeyword) {
+ deleteSearchKeywordCharacters(numberOfDeletionSteps = numberOfDeletionSteps)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun verifySuggestionsAreNotDisplayed(rule: ComposeTestRule, vararg searchSuggestions: String) {
+ Log.i(TAG, "verifySuggestionsAreNotDisplayed: Waiting for compose test rule to be idle")
+ rule.waitForIdle()
+ Log.i(TAG, "verifySuggestionsAreNotDisplayed: Waited for compose test rule to be idle")
+ for (searchSuggestion in searchSuggestions) {
+ Log.i(TAG, "verifySuggestionsAreNotDisplayed: Trying to verify that there are no $searchSuggestion related search suggestions")
+ rule.onAllNodesWithTag("mozac.awesomebar.suggestions")
+ .assertAny(
+ hasText(searchSuggestion)
+ .not(),
+ )
+ Log.i(TAG, "verifySuggestionsAreNotDisplayed: Verified that there are no $searchSuggestion related search suggestions")
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ fun verifySearchSuggestionsCount(rule: ComposeTestRule, numberOfSuggestions: Int, searchTerm: String) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "verifySearchSuggestionsCount: Started try #$i")
+ try {
+ Log.i(TAG, "verifySearchSuggestionsCount: Compose test rule is waiting for $waitingTime ms until the note count equals to: $numberOfSuggestions")
+ rule.waitUntilNodeCount(hasTestTag("mozac.awesomebar.suggestion"), numberOfSuggestions, waitingTime)
+ Log.i(TAG, "verifySearchSuggestionsCount: Compose test rule waited for $waitingTime ms until the note count equals to: $numberOfSuggestions")
+ Log.i(TAG, "verifySearchSuggestionsCount: Trying to verify that the count of the search suggestions equals: $numberOfSuggestions")
+ rule.onAllNodesWithTag("mozac.awesomebar.suggestion").assertCountEquals(numberOfSuggestions)
+ Log.i(TAG, "verifySearchSuggestionsCount: Verified that the count of the search suggestions equals: $numberOfSuggestions")
+
+ break
+ } catch (e: ComposeTimeoutException) {
+ Log.i(TAG, "verifySearchSuggestionsCount: ComposeTimeoutException caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ Log.i(TAG, "verifySearchSuggestionsCount: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "verifySearchSuggestionsCount: Clicked device back button")
+ homeScreen {
+ }.openSearch {
+ typeSearch(searchTerm)
+ }
+ }
+ }
+ }
+ }
+
+ fun verifyAllowSuggestionsInPrivateModeDialog() =
+ assertUIObjectExists(
+ itemWithText(getStringResource(R.string.search_suggestions_onboarding_title)),
+ itemWithText(getStringResource(R.string.search_suggestions_onboarding_text)),
+ itemWithText("Learn more"),
+ itemWithText(getStringResource(R.string.search_suggestions_onboarding_allow_button)),
+ itemWithText(getStringResource(R.string.search_suggestions_onboarding_do_not_allow_button)),
+ )
+
+ fun denySuggestionsInPrivateMode() {
+ Log.i(TAG, "denySuggestionsInPrivateMode: Trying to click the \"Don’t allow\" button")
+ mDevice.findObject(
+ UiSelector().text(getStringResource(R.string.search_suggestions_onboarding_do_not_allow_button)),
+ ).click()
+ Log.i(TAG, "denySuggestionsInPrivateMode: Clicked the \"Don’t allow\" button")
+ }
+
+ fun allowSuggestionsInPrivateMode() {
+ Log.i(TAG, "allowSuggestionsInPrivateMode: Trying to click the \"Allow\" button")
+ mDevice.findObject(
+ UiSelector().text(getStringResource(R.string.search_suggestions_onboarding_allow_button)),
+ ).click()
+ Log.i(TAG, "allowSuggestionsInPrivateMode: Clicked the \"Allow\" button")
+ }
+
+ fun verifySearchSelectorButton() = assertUIObjectExists(searchSelectorButton())
+
+ fun clickSearchSelectorButton() {
+ Log.i(TAG, "clickSearchSelectorButton: Waiting for $waitingTime ms for search selector button to exist")
+ searchSelectorButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickSearchSelectorButton: Waited for $waitingTime ms for search selector button to exist")
+ Log.i(TAG, "clickSearchSelectorButton: Trying to click the search selector button")
+ searchSelectorButton().click()
+ Log.i(TAG, "clickSearchSelectorButton: Clicked the search selector button")
+ }
+
+ fun verifySearchEngineIcon(name: String) = assertUIObjectExists(itemWithDescription(name))
+
+ fun verifySearchBarPlaceholder(text: String) {
+ Log.i(TAG, "verifySearchBarPlaceholder: Waiting for $waitingTime ms for the edit mode toolbar to exist")
+ browserToolbarEditView().waitForExists(waitingTime)
+ Log.i(TAG, "verifySearchBarPlaceholder: Waited for $waitingTime ms for the edit mode toolbar to exist")
+ assertItemTextEquals(browserToolbarEditView(), expectedText = text)
+ }
+
+ fun verifySearchShortcutListContains(vararg searchEngineName: String, shouldExist: Boolean = true) {
+ searchEngineName.forEach {
+ if (shouldExist) {
+ assertUIObjectExists(
+ searchShortcutList().getChild(UiSelector().text(it)),
+ )
+ } else {
+ assertUIObjectIsGone(searchShortcutList().getChild(UiSelector().text(it)))
+ }
+ }
+ }
+
+ // New unified search UI search selector.
+ fun selectTemporarySearchMethod(searchEngineName: String) {
+ Log.i(TAG, "selectTemporarySearchMethod: Trying to click the $searchEngineName search shortcut")
+ searchShortcutList().getChild(UiSelector().text(searchEngineName)).click()
+ Log.i(TAG, "selectTemporarySearchMethod: Clicked the $searchEngineName search shortcut")
+ }
+
+ fun clickScanButton() =
+ scanButton().also {
+ Log.i(TAG, "clickScanButton: Waiting for $waitingTime ms for the scan button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickScanButton: Waited for $waitingTime ms for the scan button to exist")
+ Log.i(TAG, "clickScanButton: Trying to click the scan button")
+ it.click()
+ Log.i(TAG, "clickScanButton: Clicked the scan button")
+ }
+
+ fun clickDismissPermissionRequiredDialog() {
+ Log.i(TAG, "clickDismissPermissionRequiredDialog: Waiting for $waitingTime ms for the \"Dismiss\" permission button to exist")
+ dismissPermissionButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickDismissPermissionRequiredDialog: Waited for $waitingTime ms for the \"Dismiss\" permission button to exist")
+ Log.i(TAG, "clickDismissPermissionRequiredDialog: Trying to click the \"Dismiss\" permission button")
+ dismissPermissionButton().click()
+ Log.i(TAG, "clickDismissPermissionRequiredDialog: Clicked the \"Dismiss\" permission button")
+ }
+
+ fun clickGoToPermissionsSettings() {
+ Log.i(TAG, "clickGoToPermissionsSettings: Waiting for $waitingTime ms for the \"Go To Settings\" permission button to exist")
+ goToPermissionsSettingsButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickGoToPermissionsSettings: Waited for $waitingTime ms for the \"Go To Settings\" permission button to exist")
+ Log.i(TAG, "clickGoToPermissionsSettings: Trying to click the \"Go To Settings\" permission button")
+ goToPermissionsSettingsButton().click()
+ Log.i(TAG, "clickGoToPermissionsSettings: Clicked the \"Go To Settings\" permission button")
+ }
+
+ fun verifyScannerOpen() {
+ Log.i(TAG, "verifyScannerOpen: Trying to verify that the device camera is opened or that the camera app error message exist")
+ assertTrue(
+ "$TAG: Neither the device camera was opened nor the camera app error message was displayed",
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/view_finder"))
+ .waitForExists(waitingTime) ||
+ // In case there is no camera available, an error will be shown.
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/camera_error"))
+ .exists(),
+ )
+ Log.i(TAG, "verifyScannerOpen: Verified that the device camera is opened or that the camera app error message exist")
+ }
+
+ fun typeSearch(searchTerm: String) {
+ Log.i(TAG, "typeSearch: Waiting for $waitingTime ms for the edit mode toolbar to exist")
+ browserToolbarEditView().waitForExists(waitingTime)
+ Log.i(TAG, "typeSearch: Waited for $waitingTime ms for the edit mode toolbar to exist")
+ Log.i(TAG, "typeSearch: Trying to set the edit mode toolbar text to $searchTerm")
+ browserToolbarEditView().setText(searchTerm)
+ Log.i(TAG, "typeSearch: Edit mode toolbar text was set to $searchTerm")
+ Log.i(TAG, "typeSearch: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "typeSearch: Waited for device to be idle")
+ }
+
+ fun clickClearButton() {
+ Log.i(TAG, "clickClearButton: Trying to click the clear button")
+ clearButton().click()
+ Log.i(TAG, "clickClearButton: Clicked the clear button")
+ }
+
+ fun tapOutsideToDismissSearchBar() {
+ Log.i(TAG, "tapOutsideToDismissSearchBar: Trying to perform a backward scroll action")
+ // After updating UIAutomator to 2.3.0 the click action doesn't seem to dismiss anymore the awesome bar
+ // On the other hand, the scroll action seems to be working properly and dismisses the awesome bar
+ UiScrollable(UiSelector().resourceId("$packageName:id/search_wrapper")).scrollBackward()
+ Log.i(TAG, "tapOutsideToDismissSearchBar: Performed a backward scroll action")
+ Log.i(TAG, "tapOutsideToDismissSearchBar: Waiting for $waitingTime ms for the edit mode toolbar to be gone")
+ browserToolbarEditView().waitUntilGone(waitingTime)
+ Log.i(TAG, "tapOutsideToDismissSearchBar: Waited for $waitingTime ms for the edit mode toolbar to be gone")
+ }
+
+ fun longClickToolbar() {
+ Log.i(TAG, "longClickToolbar: Waiting for $waitingTime ms for $packageName window to be updated")
+ mDevice.waitForWindowUpdate(packageName, waitingTime)
+ Log.i(TAG, "longClickToolbar: Waited for $waitingTime ms for $packageName window to be updated")
+ Log.i(TAG, "longClickToolbar: Waiting for $waitingTime ms for the awesome bar to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/awesomeBar"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "longClickToolbar: Waited for $waitingTime ms for the awesome bar to exist")
+ Log.i(TAG, "longClickToolbar: Waiting for $waitingTime ms for the toolbar to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "longClickToolbar: Waited for $waitingTime ms for the toolbar to exist")
+ Log.i(TAG, "longClickToolbar: Trying to perform long click on the toolbar")
+ mDevice.findObject(By.res("$packageName:id/toolbar")).click(LONG_CLICK_DURATION)
+ Log.i(TAG, "longClickToolbar: Performed long click on the toolbar")
+ }
+
+ fun clickPasteText() {
+ Log.i(TAG, "clickPasteText: Waiting for $waitingTime ms for the \"Paste\" option to exist")
+ mDevice.findObject(UiSelector().textContains("Paste")).waitForExists(waitingTime)
+ Log.i(TAG, "clickPasteText: Waited for $waitingTime ms for the \"Paste\" option to exist")
+ Log.i(TAG, "clickPasteText: Trying to click the \"Paste\" button")
+ mDevice.findObject(By.textContains("Paste")).click()
+ Log.i(TAG, "clickPasteText: Clicked the \"Paste\" button")
+ }
+
+ fun verifyTranslatedFocusedNavigationToolbar(toolbarHintString: String) =
+ assertItemTextContains(browserToolbarEditView(), itemText = toolbarHintString)
+
+ fun verifyTypedToolbarText(expectedText: String) {
+ Log.i(TAG, "verifyTypedToolbarText: Waiting for $waitingTime ms for the toolbar to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "verifyTypedToolbarText: Waited for $waitingTime ms for the toolbar to exist")
+ Log.i(TAG, "verifyTypedToolbarText: Waiting for $waitingTime ms for the edit mode toolbar to exist")
+ browserToolbarEditView().waitForExists(waitingTime)
+ Log.i(TAG, "verifyTypedToolbarText: Waited for $waitingTime ms for the edit mode toolbar to exist")
+ Log.i(TAG, "verifyTypedToolbarText: Trying to verify that $expectedText is visible in the toolbar")
+ onView(
+ allOf(
+ withText(expectedText),
+ withId(R.id.mozac_browser_toolbar_edit_url_view),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyTypedToolbarText: Verified that $expectedText is visible in the toolbar")
+ }
+
+ fun verifySearchBarPosition(bottomPosition: Boolean) {
+ Log.i(TAG, "verifySearchBarPosition: Trying to verify that the search bar is set to bottom: $bottomPosition")
+ onView(withId(R.id.toolbar))
+ .check(
+ if (bottomPosition) {
+ PositionAssertions.isCompletelyBelow(withId(R.id.keyboard_divider))
+ } else {
+ PositionAssertions.isCompletelyAbove(withId(R.id.keyboard_divider))
+ },
+ )
+ Log.i(TAG, "verifySearchBarPosition: Verified that the search bar is set to bottom: $bottomPosition")
+ }
+
+ fun deleteSearchKeywordCharacters(numberOfDeletionSteps: Int) {
+ for (i in 1..numberOfDeletionSteps) {
+ Log.i(TAG, "deleteSearchKeywordCharacters: Trying to click keyboard delete button $i times")
+ mDevice.pressDelete()
+ Log.i(TAG, "deleteSearchKeywordCharacters: Clicked keyboard delete button $i times")
+ Log.i(TAG, "deleteSearchKeywordCharacters: Waiting for $waitingTimeShort ms for $appName window to be updated")
+ mDevice.waitForWindowUpdate(appName, waitingTimeShort)
+ Log.i(TAG, "deleteSearchKeywordCharacters: Waited for $waitingTimeShort ms for $appName window to be updated")
+ }
+ }
+
+ class Transition {
+ private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
+
+ fun dismissSearchBar(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ try {
+ Log.i(TAG, "dismissSearchBar: Waiting for $waitingTime ms for the search wrapper to exist")
+ searchWrapper().waitForExists(waitingTime)
+ Log.i(TAG, "dismissSearchBar: Waited for $waitingTime ms for the search wrapper to exist")
+ Log.i(TAG, "dismissSearchBar: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "dismissSearchBar: Clicked device back button")
+ assertUIObjectIsGone(searchWrapper())
+ } catch (e: AssertionError) {
+ Log.i(TAG, "dismissSearchBar: AssertionError caught, executing fallback methods")
+ Log.i(TAG, "dismissSearchBar: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "dismissSearchBar: Clicked device back button")
+ assertUIObjectIsGone(searchWrapper())
+ }
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun openBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openBrowser: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "openBrowser: Waited for device to be idle")
+ Log.i(TAG, "openBrowser: Trying to set the edit mode toolbar text to: mozilla")
+ browserToolbarEditView().setText("mozilla\n")
+ Log.i(TAG, "openBrowser: Edit mode toolbar text was set to: mozilla")
+ Log.i(TAG, "openBrowser: Trying to click device enter button")
+ mDevice.pressEnter()
+ Log.i(TAG, "openBrowser: Clicked device enter button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun submitQuery(query: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ sessionLoadedIdlingResource = SessionLoadedIdlingResource()
+ Log.i(TAG, "submitQuery: Waiting for $waitingTime ms for the search wrapper to exist")
+ searchWrapper().waitForExists(waitingTime)
+ Log.i(TAG, "submitQuery: Waited for $waitingTime ms for the search wrapper to exist")
+ Log.i(TAG, "submitQuery: Trying to set the edit mode toolbar text to: $query")
+ browserToolbarEditView().setText(query)
+ Log.i(TAG, "submitQuery: Edit mode toolbar text was set to: $query")
+ Log.i(TAG, "submitQuery: Trying to click device enter button")
+ mDevice.pressEnter()
+ Log.i(TAG, "submitQuery: Clicked device enter button")
+
+ registerAndCleanupIdlingResources(sessionLoadedIdlingResource) {
+ assertUIObjectExists(itemWithResId("$packageName:id/browserLayout"))
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickSearchEngineSettings(interact: SettingsSubMenuSearchRobot.() -> Unit): SettingsSubMenuSearchRobot.Transition {
+ Log.i(TAG, "clickSearchEngineSettings: Trying to click the \"Search settings\" button")
+ searchShortcutList().getChild(UiSelector().text("Search settings")).click()
+ Log.i(TAG, "clickSearchEngineSettings: Clicked the \"Search settings\" button")
+
+ SettingsSubMenuSearchRobot().interact()
+ return SettingsSubMenuSearchRobot.Transition()
+ }
+
+ fun clickSearchSuggestion(searchSuggestion: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ mDevice.findObject(UiSelector().textContains(searchSuggestion)).also {
+ Log.i(TAG, "clickSearchSuggestion: Waiting for $waitingTime ms for search suggestion: $searchSuggestion to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickSearchSuggestion: Waited for $waitingTime ms for search suggestion: $searchSuggestion to exist")
+ Log.i(TAG, "clickSearchSuggestion: Trying to click search suggestion: $searchSuggestion and wait for $waitingTimeShort ms for a new window")
+ it.clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "clickSearchSuggestion: Clicked search suggestion: $searchSuggestion and waited for $waitingTimeShort ms for a new window")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+}
+
+private fun browserToolbarEditView() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
+
+private fun dismissPermissionButton() =
+ mDevice.findObject(UiSelector().text("DISMISS"))
+
+private fun goToPermissionsSettingsButton() =
+ mDevice.findObject(UiSelector().text("GO TO SETTINGS"))
+
+private fun scanButton() = itemWithDescription("Scan")
+
+private fun clearButton() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_clear_view"))
+
+private fun searchWrapper() = mDevice.findObject(UiSelector().resourceId("$packageName:id/search_wrapper"))
+
+private fun searchSelectorButton() = itemWithResId("$packageName:id/search_selector")
+
+private fun searchShortcutList() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_menu_recyclerView"))
+
+private fun voiceSearchButton() = mDevice.findObject(UiSelector().description("Voice search"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt
new file mode 100644
index 0000000000..dd7e95101e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt
@@ -0,0 +1,808 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.By.textContains
+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 org.hamcrest.CoreMatchers
+import org.hamcrest.CoreMatchers.endsWith
+import org.hamcrest.Matchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AppAndSystemHelper.isPackageInstalled
+import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
+import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_PLAY_SERVICES
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.hasCousin
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.settings.SupportUtils
+import org.mozilla.fenix.ui.robots.SettingsRobot.Companion.DEFAULT_APPS_SETTINGS_ACTION
+
+/**
+ * Implementation of Robot Pattern for the settings menu.
+ */
+class SettingsRobot {
+
+ // BASICS SECTION
+ fun verifyGeneralHeading() {
+ scrollToElementByText("General")
+ Log.i(TAG, "verifyGeneralHeading: Trying to verify that the \"General\" heading is visible")
+ onView(withText("General"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyGeneralHeading: Verified that the \"General\" heading is visible")
+ }
+
+ fun verifySearchButton() {
+ Log.i(TAG, "verifySearchButton: Waiting for $waitingTime ms until finding the \"Search\" button")
+ mDevice.wait(Until.findObject(By.text("Search")), waitingTime)
+ Log.i(TAG, "verifySearchButton: Waited for $waitingTime ms until the \"Search\" button was found")
+ Log.i(TAG, "verifySearchButton: Trying to verify that the \"Search\" button is visible")
+ onView(withText(R.string.preferences_search))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySearchButton: Verified that the \"Search\" button is visible")
+ }
+ fun verifyCustomizeButton() {
+ Log.i(TAG, "verifyCustomizeButton: Trying to verify that the \"Customize\" button is visible")
+ onView(withText("Customize"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCustomizeButton: Verified that the \"Customize\" button is visible")
+ }
+
+ fun verifyAccessibilityButton() {
+ Log.i(TAG, "verifyAccessibilityButton: Trying to verify that the \"Accessibility\" button is visible")
+ onView(withText("Accessibility"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAccessibilityButton: Verified that the \"Accessibility\" button is visible")
+ }
+ fun verifySetAsDefaultBrowserButton() {
+ scrollToElementByText("Set as default browser")
+ Log.i(TAG, "verifySetAsDefaultBrowserButton: Trying to verify that the \"Set as default browser\" button is visible")
+ onView(withText("Set as default browser"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySetAsDefaultBrowserButton: Verified that the \"Set as default browser\" button is visible")
+ }
+ fun verifyTabsButton() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.preferences_tabs)))
+ fun verifyHomepageButton() {
+ Log.i(TAG, "verifyHomepageButton: Trying to verify that the \"Homepage\" button is visible")
+ onView(withText(R.string.preferences_home_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomepageButton: Verified that the \"Homepage\" button is visible")
+ }
+ fun verifyAutofillButton() {
+ Log.i(TAG, "verifyAutofillButton: Trying to verify that the \"Autofill\" button is visible")
+ onView(withText(R.string.preferences_autofill)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAutofillButton: Verified that the \"Autofill\" button is visible")
+ }
+ fun verifyLanguageButton() {
+ scrollToElementByText(getStringResource(R.string.preferences_language))
+ Log.i(TAG, "verifyLanguageButton: Trying to verify that the \"Language\" button is visible")
+ onView(withText(R.string.preferences_language)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyLanguageButton: Verified that the \"Language\" button is visible")
+ }
+ fun verifyDefaultBrowserToggle(isEnabled: Boolean) {
+ scrollToElementByText(getStringResource(R.string.preferences_set_as_default_browser))
+ Log.i(TAG, "verifyDefaultBrowserToggle: Trying to verify that the \"Set as default browser\" toggle is enabled: $isEnabled")
+ onView(withText(R.string.preferences_set_as_default_browser))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withId(R.id.switch_widget),
+ if (isEnabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyDefaultBrowserToggle: Verified that the \"Set as default browser\" toggle is enabled: $isEnabled")
+ }
+
+ fun clickDefaultBrowserSwitch() = toggleDefaultBrowserSwitch()
+ fun verifyAndroidDefaultAppsMenuAppears() {
+ Log.i(TAG, "verifyAndroidDefaultAppsMenuAppears: Trying to verify that default browser apps dialog appears")
+ intended(hasAction(DEFAULT_APPS_SETTINGS_ACTION))
+ Log.i(TAG, "verifyAndroidDefaultAppsMenuAppears: Verified that the default browser apps dialog appears")
+ }
+
+ // PRIVACY SECTION
+ fun verifyPrivacyHeading() {
+ scrollToElementByText("Privacy and security")
+ Log.i(TAG, "verifyPrivacyHeading: Trying to verify that the \"Privacy and security\" heading is visible")
+ onView(withText("Privacy and security"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyPrivacyHeading: Verified that the \"Privacy and security\" heading is visible")
+ }
+ fun verifyHTTPSOnlyModeButton() {
+ scrollToElementByText(getStringResource(R.string.preferences_https_only_title))
+ Log.i(TAG, "verifyHTTPSOnlyModeButton: Trying to verify that the \"HTTPS-Only Mode\" button is visible")
+ onView(
+ withText(R.string.preferences_https_only_title),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHTTPSOnlyModeButton: Verified that the \"HTTPS-Only Mode\" button is visible")
+ }
+
+ fun verifyCookieBannerBlockerButton(enabled: Boolean) {
+ scrollToElementByText(getStringResource(R.string.preferences_cookie_banner_reduction_private_mode))
+ Log.i(TAG, "verifyCookieBannerBlockerButton: Trying to verify that the \"Cookie Banner Blocker in private browsing\" toggle is enabled: $enabled")
+ onView(withText(R.string.preferences_cookie_banner_reduction_private_mode))
+ .check(
+ matches(
+ hasCousin(
+ CoreMatchers.allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyCookieBannerBlockerButton: Verified that the \"Cookie Banner Blocker in private browsing\" toggle is enabled: $enabled")
+ }
+
+ fun verifyEnhancedTrackingProtectionButton() {
+ Log.i(TAG, "verifyEnhancedTrackingProtectionButton: Waiting for $waitingTime ms until finding the \"Privacy and Security\" heading")
+ mDevice.wait(Until.findObject(By.text("Privacy and Security")), waitingTime)
+ Log.i(TAG, "verifyEnhancedTrackingProtectionButton: Waited for $waitingTime ms until the \"Privacy and Security\" heading was found")
+ Log.i(TAG, "verifyEnhancedTrackingProtectionButton: Trying to verify that the \"Enhanced Tracking Protection\" button is visible")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Enhanced Tracking Protection")),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEnhancedTrackingProtectionButton: Verified that the \"Enhanced Tracking Protection\" button is visible")
+ }
+ fun verifyLoginsAndPasswordsButton() {
+ scrollToElementByText("Passwords")
+ Log.i(TAG, "verifyLoginsAndPasswordsButton: Trying to verify that the \"Logins and passwords\" button is visible")
+ onView(withText(R.string.preferences_passwords_logins_and_passwords_2))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyLoginsAndPasswordsButton: Verified that the \"Logins and passwords\" button is visible")
+ }
+ fun verifyPrivateBrowsingButton() {
+ scrollToElementByText("Private browsing")
+ Log.i(TAG, "verifyPrivateBrowsingButton: Waiting for $waitingTime ms until finding the \"Private browsing\" button")
+ mDevice.wait(Until.findObject(By.text("Private browsing")), waitingTime)
+ Log.i(TAG, "verifyPrivateBrowsingButton: Waited for $waitingTime ms until the \"Private browsing\" button was found")
+ Log.i(TAG, "verifyPrivateBrowsingButton: Trying to verify that the \"Private browsing\" button is visible")
+ onView(withText("Private browsing"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyPrivateBrowsingButton: Verified that the \"Private browsing\" button is visible")
+ }
+ fun verifySitePermissionsButton() {
+ scrollToElementByText("Site permissions")
+ Log.i(TAG, "verifySitePermissionsButton: Trying to verify that the \"Site permissions\" button is visible")
+ onView(withText("Site permissions"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySitePermissionsButton: Verified that the \"Site permissions\" button is visible")
+ }
+ fun verifyDeleteBrowsingDataButton() {
+ scrollToElementByText("Delete browsing data")
+ Log.i(TAG, "verifyDeleteBrowsingDataButton: Trying to verify that the \"Delete browsing data\" button is visible")
+ onView(withText("Delete browsing data"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeleteBrowsingDataButton: Verified that the \"Delete browsing data\" button is visible")
+ }
+ fun verifyDeleteBrowsingDataOnQuitButton() {
+ scrollToElementByText("Delete browsing data on quit")
+ Log.i(TAG, "verifyDeleteBrowsingDataOnQuitButton: Trying to verify that the \"Delete browsing data on quit\" button is visible")
+ onView(withText("Delete browsing data on quit"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeleteBrowsingDataOnQuitButton: Verified that the \"Delete browsing data on quit\" button is visible")
+ }
+ fun verifyNotificationsButton() {
+ scrollToElementByText("Notifications")
+ Log.i(TAG, "verifyNotificationsButton: Trying to verify that the \"Notifications\" button is visible")
+ onView(withText("Notifications"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyNotificationsButton: Verified that the \"Notifications\" button is visible")
+ }
+ fun verifyDataCollectionButton() {
+ scrollToElementByText("Data collection")
+ Log.i(TAG, "verifyDataCollectionButton: Trying to verify that the \"Data collection\" button is visible")
+ onView(withText("Data collection"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDataCollectionButton: Verified that the \"Data collection\" button is visible")
+ }
+ fun verifyOpenLinksInAppsButton() {
+ scrollToElementByText("Open links in apps")
+ Log.i(TAG, "verifyOpenLinksInAppsButton: Trying to verify that the \"Open links in apps\" button is visible")
+ openLinksInAppsButton()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyOpenLinksInAppsButton: Verified that the \"Open links in apps\" button is visible")
+ }
+ fun verifySettingsView() {
+ scrollToElementByText("General")
+ Log.i(TAG, "verifySettingsView: Trying to verify that the \"General\" heading is visible")
+ onView(withText("General"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySettingsView: Verified that the \"General\" heading is visible")
+ scrollToElementByText("Privacy and security")
+ Log.i(TAG, "verifySettingsView: Trying to verify that the \"Privacy and security\" heading is visible")
+ onView(withText("Privacy and security"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySettingsView: Verified that the \"Privacy and security\" heading is visible")
+
+ val extensions = getStringResource(R.string.preferences_extensions)
+ Log.i(TAG, "verifySettingsView: Trying to perform scroll to the \"$extensions\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(R.string.preferences_extensions)),
+ ),
+ )
+ Log.i(TAG, "verifySettingsView: Performed scroll to the \"$extensions\" button")
+ Log.i(TAG, "verifySettingsView: Trying to verify that the \"$extensions\" button is completely displayed")
+ onView(withText(R.string.preferences_extensions))
+ .check(matches(isCompletelyDisplayed()))
+ Log.i(TAG, "verifySettingsView: Verified that the \"$extensions\" button is completely displayed")
+
+ Log.i(TAG, "verifySettingsView: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the settings list")
+ settingsList().scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifySettingsView: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the settings list")
+ Log.i(TAG, "verifySettingsView: Trying to verify that the \"About\" heading is visible")
+ onView(withText("About"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySettingsView: Verified that the \"About\" heading is visible")
+ }
+ fun verifySettingsToolbar() {
+ Log.i(TAG, "verifySettingsToolbar: Trying to verify that the navigate up button is visible")
+ onView(
+ allOf(
+ withId(R.id.navigationToolbar),
+ hasDescendant(withContentDescription(R.string.action_bar_up_description)),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySettingsToolbar: Verified that the navigate up button is visible")
+ Log.i(TAG, "verifySettingsToolbar: Trying to verify that the \"Settings\" toolbar title is visible")
+ onView(
+ allOf(
+ withId(R.id.navigationToolbar),
+ hasDescendant(withText(R.string.settings)),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySettingsToolbar: Verified that the \"Settings\" toolbar title is visible")
+ }
+
+ // ADVANCED SECTION
+ fun verifyAdvancedHeading() {
+ val extensions = getStringResource(R.string.preferences_extensions)
+ Log.i(TAG, "verifyAdvancedHeading: Trying to perform scroll to the \"$extensions\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(R.string.preferences_extensions)),
+ ),
+ )
+ Log.i(TAG, "verifyAdvancedHeading: Performed scroll to the \"$extensions\" button")
+ Log.i(TAG, "verifyAdvancedHeading: Trying to verify that the \"$extensions\" button is completely displayed")
+ onView(withText(R.string.preferences_extensions))
+ .check(matches(isCompletelyDisplayed()))
+ Log.i(TAG, "verifyAdvancedHeading: Verified that the \"$extensions\" button is completely displayed")
+ }
+ fun verifyAddons() {
+ val extensions = getStringResource(R.string.preferences_extensions)
+ Log.i(TAG, "verifyAddons: Trying to perform scroll to the \"$extensions\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(R.string.preferences_extensions)),
+ ),
+ )
+ Log.i(TAG, "verifyAddons: Performed scroll to the \"$extensions\" button")
+ Log.i(TAG, "verifyAddons: Trying to verify that the \"$extensions\" button is completely displayed")
+ addonsManagerButton()
+ .check(matches(isCompletelyDisplayed()))
+ Log.i(TAG, "verifyAddons: Verified that the \"$extensions\" button is completely displayed")
+ }
+
+ fun verifyExternalDownloadManagerButton() {
+ Log.i(TAG, "verifyExternalDownloadManagerButton: Trying to verify that the \"External download manager\" button is visible")
+ onView(
+ withText(R.string.preferences_external_download_manager),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyExternalDownloadManagerButton: Verified that the \"External download manager\" button is visible")
+ }
+
+ fun verifyExternalDownloadManagerToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyExternalDownloadManagerToggle: Trying to verify that the \"External download manager\" toggle is enabled: $enabled")
+ onView(withText(R.string.preferences_external_download_manager))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyExternalDownloadManagerToggle: Verified that the \"External download manager\" toggle is enabled: $enabled")
+ }
+
+ fun verifyLeakCanaryToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyLeakCanaryToggle: Trying to verify that the \"LeakCanary\" toggle is enabled: $enabled")
+ onView(withText(R.string.preference_leakcanary))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyLeakCanaryToggle: Verified that the \"LeakCanary\" toggle is enabled: $enabled")
+ }
+
+ fun verifyRemoteDebuggingToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyRemoteDebuggingToggle: Trying to verify that the \"Remote debugging via USB\" toggle is enabled: $enabled")
+ onView(withText(R.string.preferences_remote_debugging))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyRemoteDebuggingToggle: Verified that the \"Remote debugging via USB\" toggle is enabled: $enabled")
+ }
+
+ // DEVELOPER TOOLS SECTION
+ fun verifyRemoteDebuggingButton() {
+ scrollToElementByText("Remote debugging via USB")
+ Log.i(TAG, "verifyRemoteDebuggingButton: Trying to verify that the \"Remote debugging via USB\" button is visible")
+ onView(withText("Remote debugging via USB"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyRemoteDebuggingButton: Verified that the \"Remote debugging via USB\" button is visible")
+ }
+ fun verifyLeakCanaryButton() {
+ scrollToElementByText("LeakCanary")
+ Log.i(TAG, "verifyLeakCanaryButton: Trying to verify that the \"LeakCanary\" button is visible")
+ onView(withText("LeakCanary"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyLeakCanaryButton: Verified that the \"LeakCanary\" button is visible")
+ }
+
+ // ABOUT SECTION
+ fun verifyAboutHeading() {
+ Log.i(TAG, "verifyAboutHeading: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the settings list")
+ settingsList().scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyAboutHeading: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the settings list")
+ Log.i(TAG, "verifyAboutHeading: Trying to verify that the \"About\" heading is visible")
+ onView(withText("About"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAboutHeading: Verified that the \"About\" heading is visible")
+ }
+
+ fun verifyRateOnGooglePlay() = assertUIObjectExists(rateOnGooglePlayHeading())
+ fun verifyAboutFirefoxPreview() = assertUIObjectExists(aboutFirefoxHeading())
+ fun verifyGooglePlayRedirect() {
+ if (isPackageInstalled(GOOGLE_PLAY_SERVICES)) {
+ Log.i(TAG, "verifyGooglePlayRedirect: $GOOGLE_PLAY_SERVICES is installed")
+ try {
+ Log.i(TAG, "verifyGooglePlayRedirect: Trying to verify intent to: $GOOGLE_PLAY_SERVICES")
+ intended(
+ allOf(
+ hasAction(Intent.ACTION_VIEW),
+ hasData(Uri.parse(SupportUtils.RATE_APP_URL)),
+ ),
+ )
+ Log.i(TAG, "verifyGooglePlayRedirect: Verified intent to: $GOOGLE_PLAY_SERVICES")
+ } catch (e: AssertionFailedError) {
+ Log.i(TAG, "verifyGooglePlayRedirect: AssertionFailedError caught, executing fallback methods")
+ BrowserRobot().verifyRateOnGooglePlayURL()
+ }
+ } else {
+ BrowserRobot().verifyRateOnGooglePlayURL()
+ }
+ }
+
+ fun verifySettingsOptionSummary(setting: String, summary: String) {
+ scrollToElementByText(setting)
+ Log.i(TAG, "verifySettingsOptionSummary: Trying to verify that setting: $setting with summary:$summary is visible")
+ onView(
+ allOf(
+ withText(setting),
+ hasSibling(withText(summary)),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySettingsOptionSummary: Verified that setting: $setting with summary:$summary is visible")
+ }
+
+ class Transition {
+ fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goBackToOnboardingScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBackToOnboardingScreen: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBackToOnboardingScreen: Clicked device back button")
+ Log.i(TAG, "goBackToOnboardingScreen: Waiting for device to be idle for $waitingTimeShort ms")
+ mDevice.waitForIdle(waitingTimeShort)
+ Log.i(TAG, "goBackToOnboardingScreen: Device was idle for $waitingTimeShort ms")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goBackToBrowser: Trying to click the navigate up button")
+ goBackButton().click()
+ Log.i(TAG, "goBackToBrowser: Clicked the navigate up button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openAboutFirefoxPreview(interact: SettingsSubMenuAboutRobot.() -> Unit): SettingsSubMenuAboutRobot.Transition {
+ Log.i(TAG, "openAboutFirefoxPreview: Trying to click the \"About Firefox\" button")
+ aboutFirefoxHeading().click()
+ Log.i(TAG, "openAboutFirefoxPreview: Clicked the \"About Firefox\" button")
+ SettingsSubMenuAboutRobot().interact()
+ return SettingsSubMenuAboutRobot.Transition()
+ }
+
+ fun openSearchSubMenu(interact: SettingsSubMenuSearchRobot.() -> Unit): SettingsSubMenuSearchRobot.Transition {
+ itemWithText(getStringResource(R.string.preferences_search))
+ .also {
+ Log.i(TAG, "openSearchSubMenu: Waiting for $waitingTimeShort ms for the \"Search\" button to exist")
+ it.waitForExists(waitingTimeShort)
+ Log.i(TAG, "openSearchSubMenu: Waited for $waitingTimeShort ms for the \"Search\" button to exist")
+ Log.i(TAG, "openSearchSubMenu: Trying to click the \"Search\" button")
+ it.click()
+ Log.i(TAG, "openSearchSubMenu: Clicked the \"Search\" button")
+ }
+
+ SettingsSubMenuSearchRobot().interact()
+ return SettingsSubMenuSearchRobot.Transition()
+ }
+
+ fun openCustomizeSubMenu(interact: SettingsSubMenuCustomizeRobot.() -> Unit): SettingsSubMenuCustomizeRobot.Transition {
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.preferences_customize)))
+ Log.i(TAG, "openCustomizeSubMenu: Trying to click the \"Customize\" button")
+ itemContainingText(getStringResource(R.string.preferences_customize)).click()
+ Log.i(TAG, "openCustomizeSubMenu: Clicked the \"Customize\" button")
+
+ SettingsSubMenuCustomizeRobot().interact()
+ return SettingsSubMenuCustomizeRobot.Transition()
+ }
+
+ fun openTabsSubMenu(interact: SettingsSubMenuTabsRobot.() -> Unit): SettingsSubMenuTabsRobot.Transition {
+ itemWithText(getStringResource(R.string.preferences_tabs))
+ .also {
+ Log.i(TAG, "openTabsSubMenu: Waiting for $waitingTime ms for the \"Tabs\" button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "openTabsSubMenu: Waited for $waitingTime ms for the \"Tabs\" button to exist")
+ Log.i(TAG, "openTabsSubMenu: Trying to click the \"Tabs\" button and wait for $waitingTimeShort ms for a new window")
+ it.clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "openTabsSubMenu: Clicked the \"Tabs\" button and wait for $waitingTimeShort ms for a new window")
+ }
+
+ SettingsSubMenuTabsRobot().interact()
+ return SettingsSubMenuTabsRobot.Transition()
+ }
+
+ fun openHomepageSubMenu(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
+ Log.i(TAG, "openHomepageSubMenu: Waiting for $waitingTime ms for the \"Homepage\" button to exist")
+ mDevice.findObject(UiSelector().textContains("Homepage")).waitForExists(waitingTime)
+ Log.i(TAG, "openHomepageSubMenu: Waited for $waitingTime ms for the \"Homepage\" button to exist")
+ Log.i(TAG, "openHomepageSubMenu: Trying to click the \"Homepage\" button")
+ onView(withText(R.string.preferences_home_2)).click()
+ Log.i(TAG, "openHomepageSubMenu: Clicked the \"Homepage\" button")
+
+ SettingsSubMenuHomepageRobot().interact()
+ return SettingsSubMenuHomepageRobot.Transition()
+ }
+
+ fun openAutofillSubMenu(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
+ mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_autofill)))
+ .also {
+ Log.i(TAG, "openAutofillSubMenu: Waiting for $waitingTime ms for the \"Autofill\" button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "openAutofillSubMenu: Waited for $waitingTime ms for the \"Autofill\" button to exist")
+ Log.i(TAG, "openAutofillSubMenu: Trying to click the \"Autofill\" button")
+ it.click()
+ Log.i(TAG, "openAutofillSubMenu: Clicked the \"Autofill\" button")
+ }
+
+ SettingsSubMenuAutofillRobot().interact()
+ return SettingsSubMenuAutofillRobot.Transition()
+ }
+
+ fun openAccessibilitySubMenu(interact: SettingsSubMenuAccessibilityRobot.() -> Unit): SettingsSubMenuAccessibilityRobot.Transition {
+ scrollToElementByText("Accessibility")
+ Log.i(TAG, "openAccessibilitySubMenu: Trying to verify that the \"Accessibility\" button is displayed")
+ onView(withText("Accessibility")).check(matches(isDisplayed()))
+ Log.i(TAG, "openAccessibilitySubMenu: Verified that the \"Accessibility\" button is displayed")
+ Log.i(TAG, "openAccessibilitySubMenu: Trying to click the \"Accessibility\" button")
+ onView(withText("Accessibility")).click()
+ Log.i(TAG, "openAccessibilitySubMenu: Clicked the \"Accessibility\" button")
+
+ SettingsSubMenuAccessibilityRobot().interact()
+ return SettingsSubMenuAccessibilityRobot.Transition()
+ }
+
+ fun openLanguageSubMenu(
+ localizedText: String = getStringResource(R.string.preferences_language),
+ interact: SettingsSubMenuLanguageRobot.() -> Unit,
+ ): SettingsSubMenuLanguageRobot.Transition {
+ Log.i(TAG, "openLanguageSubMenu: Trying to click the $localizedText button")
+ onView(withId(R.id.recycler_view))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(
+ withText(localizedText),
+ ),
+ ViewActions.click(),
+ ),
+ )
+ Log.i(TAG, "openLanguageSubMenu: Clicked the $localizedText button")
+
+ SettingsSubMenuLanguageRobot().interact()
+ return SettingsSubMenuLanguageRobot.Transition()
+ }
+
+ fun openSetDefaultBrowserSubMenu(interact: SettingsSubMenuSetDefaultBrowserRobot.() -> Unit): SettingsSubMenuSetDefaultBrowserRobot.Transition {
+ scrollToElementByText("Set as default browser")
+ Log.i(TAG, "openSetDefaultBrowserSubMenu: Trying to click the \"Set as default browser\" button")
+ onView(withText("Set as default browser")).click()
+ Log.i(TAG, "openSetDefaultBrowserSubMenu: Clicked the \"Set as default browser\" button")
+
+ SettingsSubMenuSetDefaultBrowserRobot().interact()
+ return SettingsSubMenuSetDefaultBrowserRobot.Transition()
+ }
+
+ fun openEnhancedTrackingProtectionSubMenu(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): SettingsSubMenuEnhancedTrackingProtectionRobot.Transition {
+ scrollToElementByText("Enhanced Tracking Protection")
+ Log.i(TAG, "openEnhancedTrackingProtectionSubMenu: Trying to click the \"Enhanced Tracking Protection\" button")
+ onView(withText("Enhanced Tracking Protection")).click()
+ Log.i(TAG, "openEnhancedTrackingProtectionSubMenu: Clicked the \"Enhanced Tracking Protection\" button")
+
+ SettingsSubMenuEnhancedTrackingProtectionRobot().interact()
+ return SettingsSubMenuEnhancedTrackingProtectionRobot.Transition()
+ }
+
+ fun openLoginsAndPasswordSubMenu(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
+ scrollToElementByText("Passwords")
+ Log.i(TAG, "openLoginsAndPasswordSubMenu: Trying to click the \"Logins and passwords\" button")
+ onView(withText("Passwords")).click()
+ Log.i(TAG, "openLoginsAndPasswordSubMenu: Clicked the \"Logins and passwords\" button")
+
+ SettingsSubMenuLoginsAndPasswordRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordRobot.Transition()
+ }
+
+ fun openTurnOnSyncMenu(interact: SettingsTurnOnSyncRobot.() -> Unit): SettingsTurnOnSyncRobot.Transition {
+ Log.i(TAG, "openTurnOnSyncMenu: Trying to click the \"Sync and save your data\" button")
+ onView(withText("Sync and save your data")).click()
+ Log.i(TAG, "openTurnOnSyncMenu: Clicked the \"Sync and save your data\" button")
+
+ SettingsTurnOnSyncRobot().interact()
+ return SettingsTurnOnSyncRobot.Transition()
+ }
+
+ fun openPrivateBrowsingSubMenu(interact: SettingsSubMenuPrivateBrowsingRobot.() -> Unit): SettingsSubMenuPrivateBrowsingRobot.Transition {
+ scrollToElementByText("Private browsing")
+ Log.i(TAG, "openPrivateBrowsingSubMenu: Trying to click the \"Private browsing\" button")
+ mDevice.findObject(textContains("Private browsing")).click()
+ Log.i(TAG, "openPrivateBrowsingSubMenu: Clicked the \"Private browsing\" button")
+
+ SettingsSubMenuPrivateBrowsingRobot().interact()
+ return SettingsSubMenuPrivateBrowsingRobot.Transition()
+ }
+
+ fun openSettingsSubMenuSitePermissions(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition {
+ scrollToElementByText("Site permissions")
+ Log.i(TAG, "openSettingsSubMenuSitePermissions: Trying to click the \"Site permissions\" button")
+ mDevice.findObject(textContains("Site permissions")).click()
+ Log.i(TAG, "openSettingsSubMenuSitePermissions: Clicked the \"Site permissions\" button")
+
+ SettingsSubMenuSitePermissionsRobot().interact()
+ return SettingsSubMenuSitePermissionsRobot.Transition()
+ }
+
+ fun openSettingsSubMenuDeleteBrowsingData(interact: SettingsSubMenuDeleteBrowsingDataRobot.() -> Unit): SettingsSubMenuDeleteBrowsingDataRobot.Transition {
+ scrollToElementByText("Delete browsing data")
+ Log.i(TAG, "openSettingsSubMenuDeleteBrowsingData: Trying to click the \"Delete browsing data\" button")
+ mDevice.findObject(textContains("Delete browsing data")).click()
+ Log.i(TAG, "openSettingsSubMenuDeleteBrowsingData: Clicked the \"Delete browsing data\" button")
+
+ SettingsSubMenuDeleteBrowsingDataRobot().interact()
+ return SettingsSubMenuDeleteBrowsingDataRobot.Transition()
+ }
+
+ fun openSettingsSubMenuDeleteBrowsingDataOnQuit(interact: SettingsSubMenuDeleteBrowsingDataOnQuitRobot.() -> Unit): SettingsSubMenuDeleteBrowsingDataOnQuitRobot.Transition {
+ scrollToElementByText("Delete browsing data on quit")
+ Log.i(TAG, "openSettingsSubMenuDeleteBrowsingDataOnQuit: Trying to click the \"Delete browsing data on quit\" button")
+ mDevice.findObject(textContains("Delete browsing data on quit")).click()
+ Log.i(TAG, "openSettingsSubMenuDeleteBrowsingDataOnQuit: Clicked the \"Delete browsing data on quit\" button")
+
+ SettingsSubMenuDeleteBrowsingDataOnQuitRobot().interact()
+ return SettingsSubMenuDeleteBrowsingDataOnQuitRobot.Transition()
+ }
+
+ fun openSettingsSubMenuNotifications(interact: SystemSettingsRobot.() -> Unit): SystemSettingsRobot.Transition {
+ scrollToElementByText("Notifications")
+ Log.i(TAG, "openSettingsSubMenuNotifications: Trying to click the \"Notifications\" button")
+ mDevice.findObject(textContains("Notifications")).click()
+ Log.i(TAG, "openSettingsSubMenuNotifications: Clicked the \"Notifications\" button")
+
+ SystemSettingsRobot().interact()
+ return SystemSettingsRobot.Transition()
+ }
+
+ fun openSettingsSubMenuDataCollection(interact: SettingsSubMenuDataCollectionRobot.() -> Unit): SettingsSubMenuDataCollectionRobot.Transition {
+ scrollToElementByText("Data collection")
+ Log.i(TAG, "openSettingsSubMenuDataCollection: Trying to click the \"Data collection\" button")
+ mDevice.findObject(textContains("Data collection")).click()
+ Log.i(TAG, "openSettingsSubMenuDataCollection: Clicked the \"Data collection\" button")
+
+ SettingsSubMenuDataCollectionRobot().interact()
+ return SettingsSubMenuDataCollectionRobot.Transition()
+ }
+
+ fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
+ Log.i(TAG, "openAddonsManagerMenu: Trying to click the \"Add-ons\" button")
+ addonsManagerButton().click()
+ Log.i(TAG, "openAddonsManagerMenu: Clicked the \"Add-ons\" button")
+
+ SettingsSubMenuAddonsManagerRobot().interact()
+ return SettingsSubMenuAddonsManagerRobot.Transition()
+ }
+
+ fun openOpenLinksInAppsMenu(interact: SettingsSubMenuOpenLinksInAppsRobot.() -> Unit): SettingsSubMenuOpenLinksInAppsRobot.Transition {
+ Log.i(TAG, "openOpenLinksInAppsMenu: Trying to click the \"Open links in apps\" button")
+ openLinksInAppsButton().click()
+ Log.i(TAG, "openOpenLinksInAppsMenu: Clicked the \"Open links in apps\" button")
+
+ SettingsSubMenuOpenLinksInAppsRobot().interact()
+ return SettingsSubMenuOpenLinksInAppsRobot.Transition()
+ }
+
+ fun openHttpsOnlyModeMenu(interact: SettingsSubMenuHttpsOnlyModeRobot.() -> Unit): SettingsSubMenuHttpsOnlyModeRobot.Transition {
+ scrollToElementByText("HTTPS-Only Mode")
+ Log.i(TAG, "openHttpsOnlyModeMenu: Trying to click the \"HTTPS-Only Mode\" button")
+ onView(withText(getStringResource(R.string.preferences_https_only_title))).click()
+ Log.i(TAG, "openHttpsOnlyModeMenu: Clicked the \"HTTPS-Only Mode\" button")
+ mDevice.waitNotNull(
+ Until.findObjects(By.res("$packageName:id/https_only_switch")),
+ waitingTime,
+ )
+
+ SettingsSubMenuHttpsOnlyModeRobot().interact()
+ return SettingsSubMenuHttpsOnlyModeRobot.Transition()
+ }
+
+ fun openExperimentsMenu(interact: SettingsSubMenuExperimentsRobot.() -> Unit): SettingsSubMenuExperimentsRobot.Transition {
+ scrollToElementByText("Nimbus Experiments")
+ onView(withText(getStringResource(R.string.preferences_nimbus_experiments))).click()
+
+ SettingsSubMenuExperimentsRobot().interact()
+ return SettingsSubMenuExperimentsRobot.Transition()
+ }
+ }
+
+ companion object {
+ const val DEFAULT_APPS_SETTINGS_ACTION = "android.app.role.action.REQUEST_ROLE"
+ }
+}
+
+fun settingsScreen(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+}
+
+private fun toggleDefaultBrowserSwitch() {
+ scrollToElementByText("Privacy and security")
+ Log.i(TAG, "toggleDefaultBrowserSwitch: Trying to click the \"Set as default browser\" button")
+ onView(withText("Set as default browser")).perform(ViewActions.click())
+ Log.i(TAG, "toggleDefaultBrowserSwitch: Clicked the \"Set as default browser\" button")
+}
+
+private fun openLinksInAppsButton() = onView(withText(R.string.preferences_open_links_in_apps))
+
+private fun rateOnGooglePlayHeading(): UiObject {
+ val rateOnGooglePlay = mDevice.findObject(UiSelector().text("Rate on Google Play"))
+ settingsList().scrollToEnd(LISTS_MAXSWIPES)
+ rateOnGooglePlay.waitForExists(waitingTime)
+
+ return rateOnGooglePlay
+}
+
+private fun aboutFirefoxHeading(): UiObject {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ settingsList().scrollToEnd(LISTS_MAXSWIPES)
+ assertUIObjectExists(itemWithText("About $appName"))
+
+ break
+ } catch (e: AssertionError) {
+ if (i == RETRY_COUNT) {
+ throw e
+ }
+ }
+ }
+ return itemContainingText("About $appName")
+}
+
+fun swipeToBottom() = onView(withId(R.id.recycler_view)).perform(ViewActions.swipeUp())
+
+fun clickRateButtonGooglePlay() {
+ rateOnGooglePlayHeading().click()
+}
+
+private fun addonsManagerButton() = onView(withText(R.string.preferences_extensions))
+
+private fun goBackButton() =
+ onView(CoreMatchers.allOf(withContentDescription("Navigate up")))
+
+private fun settingsList() =
+ UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt
new file mode 100644
index 0000000000..221a3f02fd
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt
@@ -0,0 +1,355 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.os.Build
+import android.util.Log
+import android.widget.TextView
+import androidx.core.content.pm.PackageInfoCompat
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.ViewAssertion
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.containsString
+import org.mozilla.fenix.BuildConfig
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.settings.SupportUtils
+import java.text.SimpleDateFormat
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatterBuilder
+import java.time.temporal.ChronoField
+import java.util.Calendar
+import java.util.Date
+
+/**
+ * Implementation of Robot Pattern for the settings search sub menu.
+ */
+class SettingsSubMenuAboutRobot {
+ fun verifyAboutFirefoxPreviewInfo() {
+ verifyVersionNumber()
+ verifyProductCompany()
+ verifyCurrentTimestamp()
+ verifyTheLinksList()
+ }
+
+ fun verifyVersionNumber() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0)
+ val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString()
+ val buildNVersion = "${packageInfo.versionName} (Build #$versionCode)\n"
+ val geckoVersion =
+ org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION + "-" + org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
+ val asVersion = mozilla.components.Build.applicationServicesVersion
+ Log.i(TAG, "verifyVersionNumber: Trying to verify that the about section contains build version: $buildNVersion")
+ onView(withId(R.id.about_text)).check(matches(withText(containsString(buildNVersion))))
+ Log.i(TAG, "verifyVersionNumber: Verified that the about section contains build version: $buildNVersion")
+ Log.i(TAG, "verifyVersionNumber: Trying to verify that the about section contains gecko version: $geckoVersion")
+ onView(withId(R.id.about_text)).check(matches(withText(containsString(geckoVersion))))
+ Log.i(TAG, "verifyVersionNumber: Verified that the about section contains gecko version: $geckoVersion")
+ Log.i(TAG, "verifyVersionNumber: Trying to verify that the about section contains android services version: $asVersion")
+ onView(withId(R.id.about_text)).check(matches(withText(containsString(asVersion))))
+ Log.i(TAG, "verifyVersionNumber: Verified that the about section contains android services version: $asVersion")
+ }
+
+ fun verifyProductCompany() {
+ Log.i(TAG, "verifyProductCompany: Trying to verify that the about section contains the company that produced the app info: ${"$appName is produced by Mozilla."}")
+ onView(withId(R.id.about_content))
+ .check(matches(withText(containsString("$appName is produced by Mozilla."))))
+ Log.i(TAG, "verifyProductCompany: Verified that the about section contains the company that produced the app info: \"$appName is produced by Mozilla.\"")
+ }
+
+ fun verifyCurrentTimestamp() {
+ Log.i(TAG, "verifyCurrentTimestamp: Trying to verify that the about section contains \"debug build\"")
+ onView(withId(R.id.build_date))
+ // Currently UI tests run against debug builds, which display a hard-coded string 'debug build'
+ // instead of the date. See https://github.com/mozilla-mobile/fenix/pull/10812#issuecomment-633746833
+ .check(matches(withText(containsString("debug build"))))
+ // This assertion should be valid for non-debug build types.
+ // .check(BuildDateAssertion.isDisplayedDateAccurate())
+ Log.i(TAG, "verifyCurrentTimestamp: Verified that the about section contains \"debug build\"")
+ }
+
+ fun verifyAboutToolbar() {
+ Log.i(TAG, "verifyAboutToolbar: Trying to verify that the \"About $appName\" toolbar title is visible")
+ onView(
+ allOf(
+ withId(R.id.navigationToolbar),
+ hasDescendant(withText("About $appName")),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAboutToolbar: Verified that the \"About $appName\" toolbar title is visible")
+ }
+
+ fun verifyWhatIsNewInFirefoxLink() {
+ Log.i(TAG, "verifyWhatIsNewInFirefoxLink: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyWhatIsNewInFirefoxLink: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+
+ val firefox = TestHelper.appContext.getString(R.string.firefox)
+ Log.i(TAG, "verifyWhatIsNewInFirefoxLink: Trying to verify that the \"What’s new in $firefox\" link is visible")
+ onView(withText("What’s new in $firefox")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyWhatIsNewInFirefoxLink: Verified that the \"What’s new in $firefox\" link is visible")
+ Log.i(TAG, "verifyWhatIsNewInFirefoxLink: Trying to click the \"What’s new in $firefox\" link")
+ onView(withText("What’s new in $firefox")).perform(click())
+ Log.i(TAG, "verifyWhatIsNewInFirefoxLink: Clicked the \"What’s new in $firefox\" link")
+ }
+ fun verifySupport() {
+ Log.i(TAG, "verifySupport: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifySupport: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ Log.i(TAG, "verifySupport: Trying to verify that the \"Support\" link is visible")
+ onView(withText("Support")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifySupport: Verified that the \"Support\" link is visible")
+ Log.i(TAG, "verifySupport: Trying to click the \"Support\" link")
+ onView(withText("Support")).perform(click())
+ Log.i(TAG, "verifySupport: Clicked the \"Support\" link")
+
+ TestHelper.verifyUrl(
+ "support.mozilla.org",
+ "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
+ R.id.mozac_browser_toolbar_url_view,
+ )
+ }
+
+ fun verifyCrashesLink() {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAboutFirefoxPreview {}
+ Log.i(TAG, "verifyCrashesLink: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyCrashesLink: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ Log.i(TAG, "verifyCrashesLink: Trying to verify that the \"Crashes\" link is visible")
+ onView(withText("Crashes")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCrashesLink: Verified that the \"Crashes\" link is visible")
+ Log.i(TAG, "verifyCrashesLink: Trying to click the \"Crashes\" link")
+ onView(withText("Crashes")).perform(click())
+ Log.i(TAG, "verifyCrashesLink: Clicked the \"Crashes\" link")
+
+ assertUIObjectExists(itemContainingText("No crash reports have been submitted."))
+
+ for (i in 1..3) {
+ Log.i(TAG, "verifyCrashesLink: Trying to perform press back action")
+ Espresso.pressBack()
+ Log.i(TAG, "verifyCrashesLink: Performed press back action")
+ }
+ }
+
+ fun verifyPrivacyNoticeLink() {
+ Log.i(TAG, "verifyPrivacyNoticeLink: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyPrivacyNoticeLink: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ Log.i(TAG, "verifyPrivacyNoticeLink: Trying to verify that the \"Privacy notice\" link is visible")
+ onView(withText("Privacy notice")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyPrivacyNoticeLink: Verified that the \"Privacy notice\" link is visible")
+ Log.i(TAG, "verifyPrivacyNoticeLink: Trying to click the \"Privacy notice\" link")
+ onView(withText("Privacy notice")).perform(click())
+ Log.i(TAG, "verifyPrivacyNoticeLink: Clicked the \"Privacy notice\" link")
+
+ TestHelper.verifyUrl(
+ "/privacy/firefox",
+ "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
+ R.id.mozac_browser_toolbar_url_view,
+ )
+ }
+
+ fun verifyKnowYourRightsLink() {
+ Log.i(TAG, "verifyKnowYourRightsLink: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyKnowYourRightsLink: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ Log.i(TAG, "verifyKnowYourRightsLink: Trying to verify that the \"Know your rights\" link is visible")
+ onView(withText("Know your rights")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyKnowYourRightsLink: Verified that the \"Know your rights\" link is visible")
+ Log.i(TAG, "verifyKnowYourRightsLink: Trying to click the \"Know your rights\" link")
+ onView(withText("Know your rights")).perform(click())
+ Log.i(TAG, "verifyKnowYourRightsLink: Clicked the \"Know your rights\" link")
+
+ TestHelper.verifyUrl(
+ SupportUtils.SumoTopic.YOUR_RIGHTS.topicStr,
+ "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
+ R.id.mozac_browser_toolbar_url_view,
+ )
+ }
+
+ fun verifyLicensingInformationLink() {
+ Log.i(TAG, "verifyLicensingInformationLink: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyLicensingInformationLink: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ Log.i(TAG, "verifyLicensingInformationLink: Trying to verify that the \"Licensing information\" link is visible")
+ onView(withText("Licensing information")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyLicensingInformationLink: Verified that the \"Licensing information\" link is visible")
+ Log.i(TAG, "verifyLicensingInformationLink: Trying to click the \"Licensing information\" link")
+ onView(withText("Licensing information")).perform(click())
+ Log.i(TAG, "verifyLicensingInformationLink: Clicked the \"Licensing information\" link")
+
+ TestHelper.verifyUrl(
+ "about:license",
+ "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
+ R.id.mozac_browser_toolbar_url_view,
+ )
+ }
+
+ fun verifyLibrariesUsedLink() {
+ Log.i(TAG, "verifyLibrariesUsedLink: Trying to perform ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
+ Log.i(TAG, "verifyLibrariesUsedLink: Performed ${LISTS_MAXSWIPES}x a scroll action to the end of the about list")
+ Log.i(TAG, "verifyLibrariesUsedLink: Trying to verify that the \"Libraries that we use\" link is visible")
+ onView(withText("Libraries that we use")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyLibrariesUsedLink: Verified that the \"Libraries that we use\" link is visible")
+ Log.i(TAG, "verifyLibrariesUsedLink: Trying to click the \"Libraries that we use\" link")
+ onView(withText("Libraries that we use")).perform(click())
+ Log.i(TAG, "verifyLibrariesUsedLink: Clicked the \"Libraries that we use\" link")
+ Log.i(TAG, "verifyLibrariesUsedLink: Trying to verify that the toolbar has title: \"$appName | OSS Libraries\"")
+ onView(withId(R.id.navigationToolbar)).check(matches(hasDescendant(withText(containsString("$appName | OSS Libraries")))))
+ Log.i(TAG, "verifyLibrariesUsedLink: Verified that the toolbar has title: \"$appName | OSS Libraries\"")
+ Log.i(TAG, "verifyLibrariesUsedLink: Trying to perform press back action")
+ Espresso.pressBack()
+ Log.i(TAG, "verifyLibrariesUsedLink: Performed press back action")
+ }
+
+ fun verifyTheLinksList() {
+ verifyAboutToolbar()
+ verifyWhatIsNewInFirefoxLink()
+ navigateBackToAboutPage()
+ verifySupport()
+ verifyCrashesLink()
+ navigateBackToAboutPage()
+ verifyPrivacyNoticeLink()
+ navigateBackToAboutPage()
+ verifyKnowYourRightsLink()
+ navigateBackToAboutPage()
+ verifyLicensingInformationLink()
+ navigateBackToAboutPage()
+ verifyLibrariesUsedLink()
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun navigateBackToAboutPage() {
+ navigationToolbar {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAboutFirefoxPreview {
+ }
+}
+
+private val aboutMenuList = UiScrollable(UiSelector().resourceId("$packageName:id/about_layout"))
+
+private fun goBackButton() =
+ onView(withContentDescription("Navigate up"))
+
+class BuildDateAssertion {
+ // When the app is built on firebase, there are times where the BuildDate is off by a few seconds or a few minutes.
+ // To compensate for that slight discrepancy, this assertion was added to see if the Build Date shown
+ // is within a reasonable amount of time from when the app was built.
+ companion object {
+ // this pattern represents the following date format: "Monday 12/30 @ 6:49 PM"
+ private const val DATE_PATTERN = "EEEE M/d @ h:m a"
+
+ //
+ private const val NUM_OF_HOURS = 1
+
+ fun isDisplayedDateAccurate(): ViewAssertion {
+ return ViewAssertion { view, noViewFoundException ->
+ if (noViewFoundException != null) throw noViewFoundException
+
+ val textFromView = (view as TextView).text
+ ?: throw AssertionError("This view is not of type TextView")
+
+ verifyDateIsWithinRange(textFromView.toString(), NUM_OF_HOURS)
+ }
+ }
+
+ private fun verifyDateIsWithinRange(dateText: String, hours: Int) {
+ // This assertion checks whether has defined a range of tim
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
+ val simpleDateFormat = SimpleDateFormat(DATE_PATTERN)
+ val date = simpleDateFormat.parse(dateText)
+ if (date == null || !date.isWithinRangeOf(hours)) {
+ throw AssertionError("The build date is not within Range.")
+ }
+ } else {
+ val textviewDate = getLocalDateTimeFromString(dateText)
+ val buildConfigDate = getLocalDateTimeFromString(BuildConfig.BUILD_DATE)
+
+ if (!buildConfigDate.isEqual(textviewDate) &&
+ !textviewDate.isWithinRangeOf(hours, buildConfigDate)
+ ) {
+ throw AssertionError("$textviewDate is not equal to the date within the build config: $buildConfigDate, and are not within a reasonable amount of time from each other.")
+ }
+ }
+ }
+
+ private fun Date.isWithinRangeOf(hours: Int): Boolean {
+ // To determine the date range, the maxDate is retrieved by adding the variable hours to the calendar.
+ // Since the calendar will represent the maxDate at this time, to retrieve the minDate the variable hours is multipled by negative 2 and added to the calendar
+ // This will result in the maxDate being equal to the original Date + hours, and minDate being equal to original Date - hours
+
+ val calendar = Calendar.getInstance()
+ val currentYear = calendar.get(Calendar.YEAR)
+ calendar.time = this
+ calendar.set(Calendar.YEAR, currentYear)
+ val updatedDate = calendar.time
+
+ calendar.add(Calendar.HOUR_OF_DAY, hours)
+ val maxDate = calendar.time
+ calendar.add(
+ Calendar.HOUR_OF_DAY,
+ hours * -2,
+ ) // Gets the minDate by subtracting from maxDate
+ val minDate = calendar.time
+ return updatedDate.after(minDate) && updatedDate.before(maxDate)
+ }
+
+ private fun LocalDateTime.isWithinRangeOf(
+ hours: Int,
+ baselineDate: LocalDateTime,
+ ): Boolean {
+ val upperBound = baselineDate.plusHours(hours.toLong())
+ val lowerBound = baselineDate.minusHours(hours.toLong())
+ val currentDate = this
+ return currentDate.isAfter(lowerBound) && currentDate.isBefore(upperBound)
+ }
+
+ private fun getLocalDateTimeFromString(buildDate: String): LocalDateTime {
+ val dateFormatter = DateTimeFormatterBuilder().appendPattern(DATE_PATTERN)
+ .parseDefaulting(ChronoField.YEAR, LocalDateTime.now().year.toLong())
+ .toFormatter()
+ return LocalDateTime.parse(buildDate, dateFormatter)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAccessibilityRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAccessibilityRobot.kt
new file mode 100644
index 0000000000..77c5650ab7
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAccessibilityRobot.kt
@@ -0,0 +1,199 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import android.view.KeyEvent
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_DPAD_LEFT
+import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
+import android.view.View
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.ViewAssertion
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.Matcher
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.components.Components
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.assertIsEnabled
+import org.mozilla.fenix.helpers.isEnabled
+import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.DECIMAL_CONVERSION
+import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.MIN_VALUE
+import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.STEP_SIZE
+import org.mozilla.fenix.ui.robots.SettingsSubMenuAccessibilityRobot.Companion.TEXT_SIZE
+import kotlin.math.roundToInt
+
+/**
+ * Implementation of Robot Pattern for the settings Accessibility sub menu.
+ */
+class SettingsSubMenuAccessibilityRobot {
+
+ companion object {
+ const val STEP_SIZE = 5
+ const val MIN_VALUE = 50
+ const val DECIMAL_CONVERSION = 100f
+ const val TEXT_SIZE = 16f
+ }
+
+ fun clickFontSizingSwitch() = toggleFontSizingSwitch()
+
+ fun verifyEnabledMenuItems() {
+ Log.i(TAG, "verifyEnabledMenuItems: Trying to verify that the \"Font Size\" title is visible")
+ onView(withText("Font Size")).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEnabledMenuItems: Verified that the \"Font Size\" title is visible")
+ Log.i(TAG, "verifyEnabledMenuItems: Trying to verify that the \"Font Size\" title is enabled")
+ onView(withText("Font Size")).check(matches(isEnabled(true)))
+ Log.i(TAG, "verifyEnabledMenuItems: Verified that the \"Font Size\" title is enabled")
+ Log.i(TAG, "verifyEnabledMenuItems: Trying to verify that the \"Make text on websites larger or smaller\" summary is visible")
+ onView(withText("Make text on websites larger or smaller")).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEnabledMenuItems: Verified that the \"Make text on websites larger or smaller\" summary is visible")
+ Log.i(TAG, "verifyEnabledMenuItems: Trying to verify that the \"Make text on websites larger or smaller\" summary is enabled")
+ onView(withText("Make text on websites larger or smaller")).check(matches(isEnabled(true)))
+ Log.i(TAG, "verifyEnabledMenuItems: Verified that the \"Make text on websites larger or smaller\" summary is enabled")
+ Log.i(TAG, "verifyEnabledMenuItems: Trying to verify the \"This is sample text. It is here to show how text will appear\" sample text")
+ onView(withId(org.mozilla.fenix.R.id.sampleText))
+ .check(matches(withText("This is sample text. It is here to show how text will appear when you increase or decrease the size with this setting.")))
+ Log.i(TAG, "verifyEnabledMenuItems: Verified the \"This is sample text. It is here to show how text will appear\" sample text")
+ Log.i(TAG, "verifyEnabledMenuItems: Trying to verify that the seek bar value is set to 100%")
+ onView(withId(org.mozilla.fenix.R.id.seekbar_value)).check(matches(withText("100%")))
+ Log.i(TAG, "verifyEnabledMenuItems: Verified that the seek bar value is set to 100%")
+ Log.i(TAG, "verifyEnabledMenuItems: Trying to verify that the seek bar is visible")
+ onView(withId(org.mozilla.fenix.R.id.seekbar)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEnabledMenuItems: Verified that the seek bar is visible")
+ }
+
+ fun verifyMenuItemsAreDisabled() {
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Trying to verify that the \"Font Size\" title is not enabled")
+ onView(withText("Font Size")).assertIsEnabled(false)
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Verified that the \"Font Size\" title is not enabled")
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Trying to verify that the \"Make text on websites larger or smaller\" summary is not enabled")
+ onView(withText("Make text on websites larger or smaller")).assertIsEnabled(false)
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Verified that the \"Make text on websites larger or smaller\" summary is not enabled")
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Trying to verify that the \"This is sample text. It is here to show how text will appear\" sample text is not enabled")
+ onView(withId(org.mozilla.fenix.R.id.sampleText)).assertIsEnabled(false)
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Verified that the \"This is sample text. It is here to show how text will appear\" sample text is not enabled")
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Trying to verify that the seek bar value is not enabled")
+ onView(withId(org.mozilla.fenix.R.id.seekbar_value)).assertIsEnabled(false)
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Verified that the seek bar value is not enabled")
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Trying to verify that the seek bar is not enabled")
+ onView(withId(org.mozilla.fenix.R.id.seekbar)).assertIsEnabled(false)
+ Log.i(TAG, "verifyMenuItemsAreDisabled: Verified that the seek bar is not enabled")
+ }
+
+ fun changeTextSizeSlider(seekBarPercentage: Int) = adjustTextSizeSlider(seekBarPercentage)
+
+ fun verifyTextSizePercentage(textSize: Int) {
+ Log.i(TAG, "verifyTextSizePercentage: Trying to verify that the text size percentage is set to: $textSize")
+ onView(withId(org.mozilla.fenix.R.id.sampleText))
+ .check(textSizePercentageEquals(textSize))
+ Log.i(TAG, "verifyTextSizePercentage: Verified that the text size percentage is set to: $textSize")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "goBack: Waited for device to be idle")
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().perform(click())
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun toggleFontSizingSwitch() {
+ Log.i(TAG, "toggleFontSizingSwitch: Trying to click the \"Automatic font sizing\" toggle")
+ // Toggle font size to off
+ onView(withText("Automatic font sizing"))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ .perform(click())
+ Log.i(TAG, "toggleFontSizingSwitch: Clicked the \"Automatic font sizing\" toggle")
+}
+
+private fun adjustTextSizeSlider(seekBarPercentage: Int) {
+ Log.i(TAG, "adjustTextSizeSlider: Trying to set the seek bar value to: $seekBarPercentage")
+ onView(withId(org.mozilla.fenix.R.id.seekbar))
+ .perform(SeekBarChangeProgressViewAction(seekBarPercentage))
+ Log.i(TAG, "adjustTextSizeSlider: Seek bar value was set to: $seekBarPercentage")
+}
+
+private fun goBackButton() =
+ onView(allOf(withContentDescription("Navigate up")))
+
+class SeekBarChangeProgressViewAction(val seekBarPercentage: Int) : ViewAction {
+ override fun getConstraints(): Matcher {
+ return isAssignableFrom(SeekBar::class.java)
+ }
+
+ override fun perform(uiController: UiController?, view: View?) {
+ val targetStepSize = calculateStepSizeFromPercentage(seekBarPercentage)
+ val seekbar = view as SeekBar
+ var progress = seekbar.progress
+
+ if (targetStepSize > progress) {
+ for (i in progress until targetStepSize) {
+ seekbar.onKeyDown(KEYCODE_DPAD_RIGHT, KeyEvent(ACTION_DOWN, KEYCODE_DPAD_RIGHT))
+ }
+ } else if (progress > targetStepSize) {
+ for (i in progress downTo targetStepSize) {
+ seekbar.onKeyDown(KEYCODE_DPAD_LEFT, KeyEvent(ACTION_DOWN, KEYCODE_DPAD_LEFT))
+ }
+ }
+ }
+
+ override fun getDescription(): String {
+ return "Changes the progress on a SeekBar, based on the percentage value."
+ }
+}
+
+fun textSizePercentageEquals(textSizePercentage: Int): ViewAssertion {
+ return ViewAssertion { view, noViewFoundException ->
+ if (noViewFoundException != null) throw noViewFoundException
+
+ val textView = view as TextView
+ val scaledPixels =
+ textView.textSize / InstrumentationRegistry.getInstrumentation().context.resources.displayMetrics.density
+ val currentTextSizePercentage = calculateTextPercentageFromTextSize(scaledPixels)
+
+ if (currentTextSizePercentage != textSizePercentage) throw AssertionError("The textview has a text size percentage of $currentTextSizePercentage, and does not match $textSizePercentage")
+ }
+}
+
+fun calculateTextPercentageFromTextSize(textSize: Float): Int {
+ val decimal = textSize / TEXT_SIZE
+ return (decimal * DECIMAL_CONVERSION).roundToInt()
+}
+
+fun calculateStepSizeFromPercentage(textSizePercentage: Int): Int {
+ return ((textSizePercentage - MIN_VALUE) / STEP_SIZE)
+}
+
+fun checkTextSizeOnWebsite(textSizePercentage: Int, components: Components) {
+ Log.i(TAG, "checkTextSizeOnWebsite: Trying to verify that text size on website is: $textSizePercentage")
+ // Checks the Gecko engine settings for the font size
+ val textSize = calculateStepSizeFromPercentage(textSizePercentage)
+ val newTextScale = ((textSize * STEP_SIZE) + MIN_VALUE).toFloat() / DECIMAL_CONVERSION
+ assertTrue("$TAG: text size on website was not set to: $textSizePercentage", components.core.engine.settings.fontSizeFactor == newTextScale)
+ Log.i(TAG, "checkTextSizeOnWebsite: Verified that text size on website is: $textSizePercentage")
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt
new file mode 100644
index 0000000000..43aceae29f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+@file:Suppress("DEPRECATION")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import android.view.View
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.rule.ActivityTestRule
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for a single Addon inside of the Addons Management Settings.
+ */
+
+class SettingsSubMenuAddonsManagerAddonDetailedMenuRobot {
+
+ class Transition {
+ fun goBack(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ onView(allOf(withContentDescription("Navigate up"))).click()
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsSubMenuAddonsManagerRobot().interact()
+ return SettingsSubMenuAddonsManagerRobot.Transition()
+ }
+
+ fun removeAddon(activityTestRule: ActivityTestRule, interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
+ registerAndCleanupIdlingResources(
+ ViewVisibilityIdlingResource(
+ activityTestRule.activity.findViewById(R.id.addon_container),
+ View.VISIBLE,
+ ),
+ ) {
+ Log.i(TAG, "removeAddon: Trying to verify that the remove add-on button is visible")
+ removeAddonButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "removeAddon: Verified that the remove add-on button is visible")
+ Log.i(TAG, "removeAddon: Trying to click the remove add-on button")
+ removeAddonButton().click()
+ Log.i(TAG, "removeAddon: Clicked the remove add-on button")
+ }
+
+ SettingsSubMenuAddonsManagerRobot().interact()
+ return SettingsSubMenuAddonsManagerRobot.Transition()
+ }
+ }
+}
+
+private fun removeAddonButton() =
+ onView(withId(R.id.remove_add_on))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt
new file mode 100644
index 0000000000..e7bfcdbcf1
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt
@@ -0,0 +1,316 @@
+/* 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.ui.robots
+
+import android.util.Log
+import android.widget.RelativeLayout
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
+import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.CoreMatchers.instanceOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.restartApp
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+/**
+ * Implementation of Robot Pattern for the Addons Management Settings.
+ */
+
+class SettingsSubMenuAddonsManagerRobot {
+ fun verifyAddonsListIsDisplayed(shouldBeDisplayed: Boolean) =
+ assertUIObjectExists(addonsList(), exists = shouldBeDisplayed)
+
+ fun verifyAddonDownloadOverlay() {
+ Log.i(TAG, "verifyAddonDownloadOverlay: Trying to verify that the \"Downloading and verifying extension\" prompt is displayed")
+ onView(withText(R.string.mozac_extension_install_progress_caption)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyAddonDownloadOverlay: Verified that the \"Downloading and verifying extension\" prompt is displayed")
+ }
+
+ fun verifyAddonPermissionPrompt(addonName: String) {
+ mDevice.waitNotNull(Until.findObject(By.text("Add $addonName?")), waitingTime)
+ Log.i(TAG, "verifyAddonPermissionPrompt: Trying to verify that the add-ons permission prompt items are displayed")
+ onView(
+ allOf(
+ withText("Add $addonName?"),
+ hasSibling(withText(containsString("It requires your permission to:"))),
+ hasSibling(withText("Add")),
+ hasSibling(withText("Cancel")),
+ ),
+ )
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyAddonPermissionPrompt: Verified that the add-ons permission prompt items are displayed")
+ }
+
+ fun clickInstallAddon(addonName: String) {
+ Log.i(TAG, "clickInstallAddon: Waiting for $waitingTime ms for add-ons list to exist")
+ addonsList().waitForExists(waitingTime)
+ Log.i(TAG, "clickInstallAddon: Waited for $waitingTime ms for add-ons list to exist")
+ Log.i(TAG, "clickInstallAddon: Trying to scroll into view the install $addonName button")
+ addonsList().scrollIntoView(
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/details_container")
+ .childSelector(UiSelector().text(addonName)),
+ ),
+ )
+ Log.i(TAG, "clickInstallAddon: Scrolled into view the install $addonName button")
+ Log.i(TAG, "clickInstallAddon: Trying to click the install $addonName button")
+ installButtonForAddon(addonName).click()
+ Log.i(TAG, "clickInstallAddon: Clicked the install $addonName button")
+ }
+
+ fun verifyAddonInstallCompleted(addonName: String, activityTestRule: HomeActivityIntentTestRule) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "verifyAddonInstallCompleted: Started try #$i")
+ try {
+ assertUIObjectExists(itemWithText("OK"), waitingTime = waitingTimeLong)
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyAddonInstallCompleted: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ restartApp(activityTestRule)
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openAddonsManagerMenu {
+ scrollToAddon(addonName)
+ clickInstallAddon(addonName)
+ verifyAddonPermissionPrompt(addonName)
+ acceptPermissionToInstallAddon()
+ }
+ }
+ }
+ }
+ }
+
+ fun verifyAddonInstallCompletedPrompt(addonName: String) {
+ Log.i(TAG, "verifyAddonInstallCompletedPrompt: Trying to verify that completed add-on install prompt items are visible")
+ onView(
+ allOf(
+ withText("OK"),
+ withParent(instanceOf(RelativeLayout::class.java)),
+ hasSibling(withText("$addonName has been added to $appName")),
+ hasSibling(withText("Access $addonName from the $appName menu.")),
+ hasSibling(withText("Allow in private browsing")),
+ ),
+ )
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAddonInstallCompletedPrompt: Verified that completed add-on install prompt items are visible")
+ }
+
+ fun closeAddonInstallCompletePrompt() {
+ Log.i(TAG, "closeAddonInstallCompletePrompt: Trying to click the \"OK\" button from the completed add-on install prompt")
+ onView(withText("OK")).click()
+ Log.i(TAG, "closeAddonInstallCompletePrompt: Clicked the \"OK\" button from the completed add-on install prompt")
+ }
+
+ fun verifyAddonIsInstalled(addonName: String) {
+ scrollToAddon(addonName)
+ Log.i(TAG, "verifyAddonIsInstalled: Trying to verify that the $addonName add-on was installed")
+ onView(
+ allOf(
+ withId(R.id.add_button),
+ isDescendantOfA(withId(R.id.add_on_item)),
+ hasSibling(hasDescendant(withText(addonName))),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.INVISIBLE)))
+ Log.i(TAG, "verifyAddonIsInstalled: Verified that the $addonName add-on was installed")
+ }
+
+ fun verifyEnabledTitleDisplayed() {
+ Log.i(TAG, "verifyEnabledTitleDisplayed: Trying to verify that the \"Enabled\" heading is displayed")
+ onView(withText("Enabled"))
+ .check(matches(isCompletelyDisplayed()))
+ Log.i(TAG, "verifyEnabledTitleDisplayed: Verified that the \"Enabled\" heading is displayed")
+ }
+
+ fun cancelInstallAddon() = cancelInstall()
+ fun acceptPermissionToInstallAddon() = allowPermissionToInstall()
+ fun verifyAddonsItems() {
+ Log.i(TAG, "verifyAddonsItems: Trying to verify that the \"Recommended\" heading is visible")
+ onView(allOf(withId(R.id.title), withText("Recommended")))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAddonsItems: Verified that the \"Recommended\" heading is visible")
+ Log.i(TAG, "verifyAddonsItems: Trying to verify that all uBlock Origin items are completely displayed")
+ onView(
+ allOf(
+ isAssignableFrom(RelativeLayout::class.java),
+ withId(R.id.add_on_item),
+ hasDescendant(allOf(withId(R.id.add_on_icon), isCompletelyDisplayed())),
+ hasDescendant(
+ allOf(
+ withId(R.id.details_container),
+ hasDescendant(withText("uBlock Origin")),
+ hasDescendant(withText("Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.")),
+ hasDescendant(withId(R.id.rating)),
+ hasDescendant(withId(R.id.review_count)),
+ ),
+ ),
+ hasDescendant(withId(R.id.add_button)),
+ ),
+ ).check(matches(isCompletelyDisplayed()))
+ Log.i(TAG, "verifyAddonsItems: Verified that all uBlock Origin items are completely displayed")
+ }
+ fun verifyAddonCanBeInstalled(addonName: String) {
+ scrollToAddon(addonName)
+ mDevice.waitNotNull(Until.findObject(By.text(addonName)), waitingTime)
+ Log.i(TAG, "verifyAddonCanBeInstalled: Trying to verify that the install $addonName button is visible")
+ onView(
+ allOf(
+ withId(R.id.add_button),
+ hasSibling(
+ hasDescendant(
+ allOf(
+ withId(R.id.add_on_name),
+ withText(addonName),
+ ),
+ ),
+ ),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAddonCanBeInstalled: Verified that the install $addonName button is visible")
+ }
+
+ fun selectAllowInPrivateBrowsing() {
+ assertUIObjectExists(itemWithText("Allow in private browsing"), waitingTime = waitingTimeLong)
+ Log.i(TAG, "selectAllowInPrivateBrowsing: Trying to click the \"Allow in private browsing\" check box")
+ onView(withId(R.id.allow_in_private_browsing)).click()
+ Log.i(TAG, "selectAllowInPrivateBrowsing: Clicked the \"Allow in private browsing\" check box")
+ }
+
+ fun installAddon(addonName: String, activityTestRule: HomeActivityIntentTestRule) {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openAddonsManagerMenu {
+ clickInstallAddon(addonName)
+ verifyAddonPermissionPrompt(addonName)
+ acceptPermissionToInstallAddon()
+ verifyAddonInstallCompleted(addonName, activityTestRule)
+ }
+ }
+
+ class Transition {
+ fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click navigate up toolbar button")
+ onView(allOf(withContentDescription("Navigate up"))).click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun openDetailedMenuForAddon(
+ addonName: String,
+ interact: SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.() -> Unit,
+ ): SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.Transition {
+ scrollToAddon(addonName)
+ Log.i(TAG, "openDetailedMenuForAddon: Trying to verify that the $addonName add-on is visible")
+ addonItem(addonName).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "openDetailedMenuForAddon: Verified that the $addonName add-on is visible")
+ Log.i(TAG, "openDetailedMenuForAddon: Trying to click the $addonName add-on")
+ addonItem(addonName).perform(click())
+ Log.i(TAG, "openDetailedMenuForAddon: Clicked the $addonName add-on")
+
+ SettingsSubMenuAddonsManagerAddonDetailedMenuRobot().interact()
+ return SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.Transition()
+ }
+ }
+
+ private fun installButtonForAddon(addonName: String) =
+ onView(
+ allOf(
+ withContentDescription("Install $addonName"),
+ isDescendantOfA(withId(R.id.add_on_item)),
+ hasSibling(hasDescendant(withText(addonName))),
+ ),
+ )
+
+ private fun cancelInstall() {
+ Log.i(TAG, "cancelInstall: Trying to verify that the \"Cancel\" button is completely displayed")
+ onView(allOf(withId(R.id.deny_button), withText("Cancel"))).check(matches(isCompletelyDisplayed()))
+ Log.i(TAG, "cancelInstall: Verified that the \"Cancel\" button is completely displayed")
+ Log.i(TAG, "cancelInstall: Trying to click the \"Cancel\" button")
+ onView(allOf(withId(R.id.deny_button), withText("Cancel"))).perform(click())
+ Log.i(TAG, "cancelInstall: Clicked the \"Cancel\" button")
+ }
+
+ private fun allowPermissionToInstall() {
+ mDevice.waitNotNull(Until.findObject(By.text("Add")), waitingTime)
+ Log.i(TAG, "allowPermissionToInstall: Trying to verify that the \"Add\" button is completely displayed")
+ onView(allOf(withId(R.id.allow_button), withText("Add"))).check(matches(isCompletelyDisplayed()))
+ Log.i(TAG, "allowPermissionToInstall: Verified that the \"Add\" button is completely displayed")
+ Log.i(TAG, "allowPermissionToInstall: Trying to click the \"Add\" button")
+ onView(allOf(withId(R.id.allow_button), withText("Add"))).perform(click())
+ Log.i(TAG, "allowPermissionToInstall: Clicked the \"Add\" button")
+ }
+}
+
+fun addonsMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
+ SettingsSubMenuAddonsManagerRobot().interact()
+ return SettingsSubMenuAddonsManagerRobot.Transition()
+}
+
+private fun scrollToAddon(addonName: String) {
+ Log.i(TAG, "scrollToAddon: Trying to scroll into view add-on: $addonName")
+ addonsList().scrollIntoView(
+ itemWithResIdContainingText(
+ resourceId = "$packageName:id/add_on_name",
+ text = addonName,
+ ),
+ )
+ Log.i(TAG, "scrollToAddon: Scrolled into view add-on: $addonName")
+}
+
+private fun addonItem(addonName: String) =
+ onView(
+ allOf(
+ withId(R.id.add_on_item),
+ hasDescendant(
+ allOf(
+ withId(R.id.add_on_name),
+ withText(addonName),
+ ),
+ ),
+ ),
+ )
+
+private fun addonsList() =
+ UiScrollable(UiSelector().resourceId("$packageName:id/add_ons_list")).setAsVerticalList()
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt
new file mode 100644
index 0000000000..e8ca4ca4ad
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt
@@ -0,0 +1,646 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.closeSoftKeyboard
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import androidx.test.espresso.matcher.ViewMatchers.withChild
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.endsWith
+import org.junit.Assert.assertEquals
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.hasCousin
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.click
+
+class SettingsSubMenuAutofillRobot {
+
+ fun verifyAutofillToolbarTitle() {
+ assertUIObjectExists(autofillToolbarTitle())
+ }
+ fun verifyManageAddressesToolbarTitle() {
+ Log.i(TAG, "verifyManageAddressesToolbarTitle: Trying to verify that the \"Manage addresses\" toolbar title is displayed")
+ onView(
+ allOf(
+ withId(R.id.navigationToolbar),
+ withChild(
+ withText(R.string.preferences_addresses_manage_addresses),
+ ),
+ ),
+ ).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyManageAddressesToolbarTitle: Verified that the \"Manage addresses\" toolbar title is displayed")
+ }
+
+ fun verifyAddressAutofillSection(isAddressAutofillEnabled: Boolean, userHasSavedAddress: Boolean) {
+ assertUIObjectExists(
+ autofillToolbarTitle(),
+ addressesSectionTitle(),
+ saveAndAutofillAddressesOption(),
+ saveAndAutofillAddressesSummary(),
+ )
+
+ if (userHasSavedAddress) {
+ assertUIObjectExists(manageAddressesButton())
+ } else {
+ assertUIObjectExists(addAddressButton())
+ }
+
+ verifyAddressesAutofillToggle(isAddressAutofillEnabled)
+ }
+
+ fun verifyCreditCardsAutofillSection(isAddressAutofillEnabled: Boolean, userHasSavedCreditCard: Boolean) {
+ assertUIObjectExists(
+ autofillToolbarTitle(),
+ creditCardsSectionTitle(),
+ saveAndAutofillCreditCardsOption(),
+ saveAndAutofillCreditCardsSummary(),
+ syncCreditCardsAcrossDevicesButton(),
+
+ )
+
+ if (userHasSavedCreditCard) {
+ assertUIObjectExists(manageSavedCreditCardsButton())
+ } else {
+ assertUIObjectExists(addCreditCardButton())
+ }
+
+ verifySaveAndAutofillCreditCardsToggle(isAddressAutofillEnabled)
+ }
+
+ fun verifyManageAddressesSection(vararg savedAddressDetails: String) {
+ assertUIObjectExists(
+ navigateBackButton(),
+ manageAddressesToolbarTitle(),
+ addAddressButton(),
+ )
+ for (savedAddressDetail in savedAddressDetails) {
+ assertUIObjectExists(itemContainingText(savedAddressDetail))
+ }
+ }
+
+ fun verifySavedCreditCardsSection(creditCardLastDigits: String, creditCardExpiryDate: String) {
+ assertUIObjectExists(
+ navigateBackButton(),
+ savedCreditCardsToolbarTitle(),
+ addCreditCardButton(),
+ itemContainingText(creditCardLastDigits),
+ itemContainingText(creditCardExpiryDate),
+ )
+ }
+
+ fun verifyAddressesAutofillToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyAddressesAutofillToggle: Trying to verify that the \"Save and autofill addresses\" toggle is checked: $enabled")
+ onView(withText(R.string.preferences_addresses_save_and_autofill_addresses_2))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyAddressesAutofillToggle: Verified that the \"Save and autofill addresses\" toggle is checked: $enabled")
+ }
+
+ fun verifySaveAndAutofillCreditCardsToggle(enabled: Boolean) {
+ Log.i(TAG, "verifySaveAndAutofillCreditCardsToggle: Trying to verify that the \"Save and autofill cards\" toggle is checked: $enabled")
+ onView(withText(R.string.preferences_credit_cards_save_and_autofill_cards_2))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifySaveAndAutofillCreditCardsToggle: Verified that the \"Save and autofill cards\" toggle is checked: $enabled")
+ }
+
+ fun verifyAddAddressView() {
+ Log.i(TAG, "verifyAddAddressView: Trying to perform \"Close soft keyboard\" action")
+ // Closing the keyboard to ensure full visibility of the "Add address" view
+ closeSoftKeyboard()
+ Log.i(TAG, "verifyAddAddressView: Performed \"Close soft keyboard\" action")
+ assertUIObjectExists(
+ addAddressToolbarTitle(),
+ navigateBackButton(),
+ toolbarCheckmarkButton(),
+ nameTextInput(),
+ streetAddressTextInput(),
+ cityTextInput(),
+ subRegionDropDown(),
+ )
+ assertUIObjectExists(
+ zipCodeTextInput(),
+ countryDropDown(),
+ phoneTextInput(),
+ emailTextInput(),
+ )
+ scrollToElementByText(getStringResource(R.string.addresses_save_button))
+ assertUIObjectExists(
+ saveButton(),
+ cancelButton(),
+ )
+ }
+
+ fun verifyCountryOption(country: String) {
+ Log.i(TAG, "verifyCountryOption: Trying to perform \"Close soft keyboard\" action")
+ // Closing the keyboard to ensure full visibility of the "Add address" view
+ closeSoftKeyboard()
+ Log.i(TAG, "verifyCountryOption: Performed \"Close soft keyboard\" action")
+ assertUIObjectExists(itemContainingText(country))
+ }
+
+ fun verifyStateOption(state: String) {
+ assertUIObjectExists(itemContainingText(state))
+ }
+
+ fun verifyCountryOptions(vararg countries: String) {
+ Log.i(TAG, "verifyCountryOptions: Trying to click the \"Country or region\" dropdown")
+ countryDropDown().click()
+ Log.i(TAG, "verifyCountryOptions: Clicked the \"Country or region\" dropdown")
+ for (country in countries) {
+ assertUIObjectExists(itemContainingText(country))
+ }
+ }
+
+ fun selectCountry(country: String) {
+ Log.i(TAG, "selectCountry: Trying to click the \"Country or region\" dropdown")
+ countryDropDown().click()
+ Log.i(TAG, "selectCountry: Clicked the \"Country or region\" dropdown")
+ Log.i(TAG, "selectCountry: Trying to select $country dropdown option")
+ countryOption(country).click()
+ Log.i(TAG, "selectCountry: Selected $country dropdown option")
+ }
+
+ fun verifyEditAddressView() {
+ assertUIObjectExists(
+ editAddressToolbarTitle(),
+ navigateBackButton(),
+ toolbarDeleteAddressButton(),
+ toolbarCheckmarkButton(),
+ nameTextInput(),
+ streetAddressTextInput(),
+ cityTextInput(),
+ subRegionDropDown(),
+ )
+ scrollToElementByText(getStringResource(R.string.addresses_country))
+ assertUIObjectExists(
+ zipCodeTextInput(),
+ countryDropDown(),
+ phoneTextInput(),
+ emailTextInput(),
+ )
+ scrollToElementByText(getStringResource(R.string.addresses_save_button))
+ assertUIObjectExists(
+ saveButton(),
+ cancelButton(),
+ )
+ assertUIObjectExists(deleteAddressButton())
+ }
+
+ fun clickSaveAndAutofillAddressesOption() {
+ Log.i(TAG, "clickSaveAndAutofillAddressesOption: Trying to click the \"Save and fill addresses\" button")
+ saveAndAutofillAddressesOption().click()
+ Log.i(TAG, "clickSaveAndAutofillAddressesOption: Clicked the \"Save and fill addresses\" button")
+ }
+ fun clickAddAddressButton() {
+ Log.i(TAG, "clickAddAddressButton: Trying to click the \"Add address\" button")
+ addAddressButton().click()
+ Log.i(TAG, "clickAddAddressButton: Clicked the \"Add address\" button")
+ }
+ fun clickManageAddressesButton() {
+ Log.i(TAG, "clickManageAddressesButton: Trying to click the \"Manage addresses\" button")
+ manageAddressesButton().click()
+ Log.i(TAG, "clickManageAddressesButton: Clicked the \"Manage addresses\" button")
+ }
+ fun clickSavedAddress(name: String) {
+ Log.i(TAG, "clickSavedAddress: Trying to click the $name saved address and and wait for $waitingTime ms for a new window")
+ savedAddress(name).clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickSavedAddress: Clicked the $name saved address and and waited for $waitingTime ms for a new window")
+ }
+ fun clickDeleteAddressButton() {
+ Log.i(TAG, "clickDeleteAddressButton: Waiting for $waitingTime ms for the delete address toolbar button to exist")
+ toolbarDeleteAddressButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickDeleteAddressButton: Waited for $waitingTime ms for the delete address toolbar button to exist")
+ Log.i(TAG, "clickDeleteAddressButton: Trying to click the delete address toolbar button")
+ toolbarDeleteAddressButton().click()
+ Log.i(TAG, "clickDeleteAddressButton: Clicked the delete address toolbar button")
+ }
+ fun clickCancelDeleteAddressButton() {
+ Log.i(TAG, "clickCancelDeleteAddressButton: Trying to click the \"CANCEL\" button from the delete address dialog")
+ cancelDeleteAddressButton().click()
+ Log.i(TAG, "clickCancelDeleteAddressButton: Clicked the \"CANCEL\" button from the delete address dialog")
+ }
+
+ fun clickConfirmDeleteAddressButton() {
+ Log.i(TAG, "clickConfirmDeleteAddressButton: Trying to click the \"DELETE\" button from the delete address dialog")
+ confirmDeleteAddressButton().click()
+ Log.i(TAG, "clickConfirmDeleteAddressButton: Clicked \"DELETE\" button from the delete address dialog")
+ }
+
+ fun clickSubRegionOption(subRegion: String) {
+ scrollToElementByText(subRegion)
+ subRegionOption(subRegion).also {
+ Log.i(TAG, "clickSubRegionOption: Waiting for $waitingTime ms for the \"State\" $subRegion dropdown option to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickSubRegionOption: Waited for $waitingTime ms for the \"State\" $subRegion dropdown option to exist")
+ Log.i(TAG, "clickSubRegionOption: Trying to click the \"State\" $subRegion dropdown option")
+ it.click()
+ Log.i(TAG, "clickSubRegionOption: Clicked the \"State\" $subRegion dropdown option")
+ }
+ }
+ fun clickCountryOption(country: String) {
+ Log.i(TAG, "clickCountryOption: Waiting for $waitingTime ms for the \"Country or region\" $country dropdown option to exist")
+ countryOption(country).waitForExists(waitingTime)
+ Log.i(TAG, "clickCountryOption: Waited for $waitingTime ms for the \"Country or region\" $country dropdown option to exist")
+ Log.i(TAG, "clickCountryOption: Trying to click \"Country or region\" $country dropdown option")
+ countryOption(country).click()
+ Log.i(TAG, "clickCountryOption: Clicked \"Country or region\" $country dropdown option")
+ }
+ fun verifyAddAddressButton() = assertUIObjectExists(addAddressButton())
+
+ fun fillAndSaveAddress(
+ navigateToAutofillSettings: Boolean,
+ isAddressAutofillEnabled: Boolean = true,
+ userHasSavedAddress: Boolean = false,
+ name: String,
+ streetAddress: String,
+ city: String,
+ state: String,
+ zipCode: String,
+ country: String,
+ phoneNumber: String,
+ emailAddress: String,
+ ) {
+ if (navigateToAutofillSettings) {
+ homeScreen {
+ }.openThreeDotMenu {
+ }.openSettings {
+ }.openAutofillSubMenu {
+ verifyAddressAutofillSection(isAddressAutofillEnabled, userHasSavedAddress)
+ clickAddAddressButton()
+ }
+ }
+ Log.i(TAG, "fillAndSaveAddress: Waiting for $waitingTime ms for \"Name\" text field to exist")
+ nameTextInput().waitForExists(waitingTime)
+ Log.i(TAG, "fillAndSaveAddress: Waited for $waitingTime ms for \"Name\" text field to exist")
+ Log.i(TAG, "fillAndSaveAddress: Trying to click device back button to dismiss keyboard using device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "fillAndSaveAddress: Clicked device back button to dismiss keyboard using device back button")
+ Log.i(TAG, "fillAndSaveAddress: Trying to set \"Name\" to $name")
+ nameTextInput().setText(name)
+ Log.i(TAG, "fillAndSaveAddress: \"Name\" was set to $name")
+ Log.i(TAG, "fillAndSaveAddress: Trying to set \"Street Address\" to $streetAddress")
+ streetAddressTextInput().setText(streetAddress)
+ Log.i(TAG, "fillAndSaveAddress: \"Street Address\" was set to $streetAddress")
+ Log.i(TAG, "fillAndSaveAddress: Trying to set \"City\" to $city")
+ cityTextInput().setText(city)
+ Log.i(TAG, "fillAndSaveAddress: \"City\" was set to $city")
+ Log.i(TAG, "fillAndSaveAddress: Trying to click \"State\" dropdown button")
+ subRegionDropDown().click()
+ Log.i(TAG, "fillAndSaveAddress: Clicked \"State\" dropdown button")
+ Log.i(TAG, "fillAndSaveAddress: Trying to click the $state dropdown option")
+ clickSubRegionOption(state)
+ Log.i(TAG, "fillAndSaveAddress: Clicked $state dropdown option")
+ Log.i(TAG, "fillAndSaveAddress: Trying to set \"Zip\" to $zipCode")
+ zipCodeTextInput().setText(zipCode)
+ Log.i(TAG, "fillAndSaveAddress: \"Zip\" was set to $zipCode")
+ Log.i(TAG, "fillAndSaveAddress: Trying to click \"Country or region\" dropdown button")
+ countryDropDown().click()
+ Log.i(TAG, "fillAndSaveAddress: Clicked \"Country or region\" dropdown button")
+ Log.i(TAG, "fillAndSaveAddress: Trying to click $country dropdown option")
+ clickCountryOption(country)
+ Log.i(TAG, "fillAndSaveAddress: Clicked $country dropdown option")
+ scrollToElementByText(getStringResource(R.string.addresses_save_button))
+ Log.i(TAG, "fillAndSaveAddress: Trying to set \"Phone\" to $phoneNumber")
+ phoneTextInput().setText(phoneNumber)
+ Log.i(TAG, "fillAndSaveAddress: \"Phone\" was set to $phoneNumber")
+ Log.i(TAG, "fillAndSaveAddress: Trying to set \"Email\" to $emailAddress")
+ emailTextInput().setText(emailAddress)
+ Log.i(TAG, "fillAndSaveAddress: \"Email\" was set to $emailAddress")
+ Log.i(TAG, "fillAndSaveAddress: Trying to click the \"Save\" button")
+ saveButton().click()
+ Log.i(TAG, "fillAndSaveAddress: Clicked the \"Save\" button")
+ Log.i(TAG, "fillAndSaveAddress: Waiting for $waitingTime ms for for \"Manage addresses\" button to exist")
+ manageAddressesButton().waitForExists(waitingTime)
+ Log.i(TAG, "fillAndSaveAddress: Waited for $waitingTime ms for for \"Manage addresses\" button to exist")
+ }
+
+ fun clickAddCreditCardButton() {
+ Log.i(TAG, "clickAddCreditCardButton: Trying to click the \"Add credit card\" button")
+ addCreditCardButton().click()
+ Log.i(TAG, "clickAddCreditCardButton: Clicked the \"Add credit card\" button")
+ }
+ fun clickManageSavedCreditCardsButton() {
+ Log.i(TAG, "clickManageSavedCreditCardsButton: Trying to click the \"Manage saved cards\" button")
+ manageSavedCreditCardsButton().click()
+ Log.i(TAG, "clickManageSavedCreditCardsButton: Clicked the \"Manage saved cards\" button")
+ }
+ fun clickSecuredCreditCardsLaterButton() {
+ Log.i(TAG, "clickSecuredCreditCardsLaterButton: Trying to click the \"Later\" button")
+ securedCreditCardsLaterButton().click()
+ Log.i(TAG, "clickSecuredCreditCardsLaterButton: Clicked the \"Later\" button")
+ }
+ fun clickSavedCreditCard() {
+ Log.i(TAG, "clickSavedCreditCard: Trying to click the saved credit card and and wait for $waitingTime ms for a new window")
+ savedCreditCardNumber().clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "clickSavedCreditCard: Clicked the saved credit card and and waited for $waitingTime ms for a new window")
+ }
+ fun clickDeleteCreditCardToolbarButton() {
+ Log.i(TAG, "clickDeleteCreditCardToolbarButton: Waiting for $waitingTime ms for the delete credit card toolbar button to exist")
+ deleteCreditCardToolbarButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickDeleteCreditCardToolbarButton: Waited for $waitingTime ms for the delete credit card toolbar button to exist")
+ Log.i(TAG, "clickDeleteCreditCardToolbarButton: Trying to click the delete credit card toolbar button")
+ deleteCreditCardToolbarButton().click()
+ Log.i(TAG, "clickDeleteCreditCardToolbarButton: Clicked the delete credit card toolbar button")
+ }
+ fun clickDeleteCreditCardMenuButton() {
+ Log.i(TAG, "clickDeleteCreditCardMenuButton: Waiting for $waitingTime ms for the delete credit card menu button to exist")
+ deleteCreditCardMenuButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickDeleteCreditCardMenuButton: Waited for $waitingTime ms for the delete credit card menu button to exist")
+ Log.i(TAG, "clickDeleteCreditCardMenuButton: Trying to click the delete credit card menu button")
+ deleteCreditCardMenuButton().click()
+ Log.i(TAG, "clickDeleteCreditCardMenuButton: Clicked the delete credit card menu button")
+ }
+ fun clickSaveAndAutofillCreditCardsOption() {
+ Log.i(TAG, "clickSaveAndAutofillCreditCardsOption: Trying to click the \"Save and autofill cards\" option")
+ saveAndAutofillCreditCardsOption().click()
+ Log.i(TAG, "clickSaveAndAutofillCreditCardsOption: Clicked the \"Save and autofill cards\" option")
+ }
+
+ fun clickConfirmDeleteCreditCardButton() {
+ Log.i(TAG, "clickConfirmDeleteCreditCardButton: Trying to click the \"Delete\" credit card dialog button")
+ confirmDeleteCreditCardButton().click()
+ Log.i(TAG, "clickConfirmDeleteCreditCardButton: Clicked the \"Delete\" credit card dialog button")
+ }
+
+ fun clickCancelDeleteCreditCardButton() {
+ Log.i(TAG, "clickCancelDeleteCreditCardButton: Trying to click the \"Cancel\" credit card dialog button")
+ cancelDeleteCreditCardButton().click()
+ Log.i(TAG, "clickCancelDeleteCreditCardButton: Clicked the \"Cancel\" credit card dialog button")
+ }
+
+ fun clickExpiryMonthOption(expiryMonth: String) {
+ Log.i(TAG, "clickExpiryMonthOption: Waiting for $waitingTime ms for the $expiryMonth expiry month option to exist")
+ expiryMonthOption(expiryMonth).waitForExists(waitingTime)
+ Log.i(TAG, "clickExpiryMonthOption: Waited for $waitingTime ms for the $expiryMonth expiry month option to exist")
+ Log.i(TAG, "clickExpiryMonthOption: Trying to click $expiryMonth expiry month option")
+ expiryMonthOption(expiryMonth).click()
+ Log.i(TAG, "clickExpiryMonthOption: Clicked $expiryMonth expiry month option")
+ }
+
+ fun clickExpiryYearOption(expiryYear: String) {
+ Log.i(TAG, "clickExpiryYearOption: Waiting for $waitingTime ms for the $expiryYear expiry year option to exist")
+ expiryYearOption(expiryYear).waitForExists(waitingTime)
+ Log.i(TAG, "clickExpiryYearOption: Waited for $waitingTime ms for the $expiryYear expiry year option to exist")
+ Log.i(TAG, "clickExpiryYearOption: Trying to click $expiryYear expiry year option")
+ expiryYearOption(expiryYear).click()
+ Log.i(TAG, "clickExpiryYearOption: Clicked $expiryYear expiry year option")
+ }
+
+ fun verifyAddCreditCardsButton() = assertUIObjectExists(addCreditCardButton())
+
+ fun fillAndSaveCreditCard(cardNumber: String, cardName: String, expiryMonth: String, expiryYear: String) {
+ Log.i(TAG, "fillAndSaveCreditCard: Waiting for $waitingTime ms for the credit card number text field to exist")
+ creditCardNumberTextInput().waitForExists(waitingTime)
+ Log.i(TAG, "fillAndSaveCreditCard: Waited for $waitingTime ms for the credit card number text field to exist")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to set the credit card number to: $cardNumber")
+ creditCardNumberTextInput().setText(cardNumber)
+ Log.i(TAG, "fillAndSaveCreditCard: The credit card number was set to: $cardNumber")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to set the name on card to: $cardName")
+ nameOnCreditCardTextInput().setText(cardName)
+ Log.i(TAG, "fillAndSaveCreditCard: The credit card name was set to: $cardName")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to click the expiry month dropdown")
+ expiryMonthDropDown().click()
+ Log.i(TAG, "fillAndSaveCreditCard: Clicked the expiry month dropdown")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to click $expiryMonth expiry month option")
+ clickExpiryMonthOption(expiryMonth)
+ Log.i(TAG, "fillAndSaveCreditCard: Clicked $expiryMonth expiry month option")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to click the expiry year dropdown")
+ expiryYearDropDown().click()
+ Log.i(TAG, "fillAndSaveCreditCard: Clicked the expiry year dropdown")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to click $expiryYear expiry year option")
+ clickExpiryYearOption(expiryYear)
+ Log.i(TAG, "fillAndSaveCreditCard: Clicked $expiryYear expiry year option")
+ Log.i(TAG, "fillAndSaveCreditCard: Trying to click the \"Save\" button")
+ saveButton().click()
+ Log.i(TAG, "fillAndSaveCreditCard: Clicked the \"Save\" button")
+ Log.i(TAG, "fillAndSaveCreditCard: Waiting for $waitingTime ms for the \"Manage saved cards\" button to exist")
+ manageSavedCreditCardsButton().waitForExists(waitingTime)
+ Log.i(TAG, "fillAndSaveCreditCard: Waited for $waitingTime ms for the \"Manage saved cards\" button to exist")
+ }
+
+ fun clearCreditCardNumber() =
+ creditCardNumberTextInput().also {
+ Log.i(TAG, "clearCreditCardNumber: Waiting for $waitingTime ms for the credit card number text field to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clearCreditCardNumber: Waited for $waitingTime ms for the credit card number text field to exist")
+ Log.i(TAG, "clearCreditCardNumber: Trying to clear the credit card number text field")
+ it.clearTextField()
+ Log.i(TAG, "clearCreditCardNumber: Cleared the credit card number text field")
+ }
+
+ fun clearNameOnCreditCard() =
+ nameOnCreditCardTextInput().also {
+ Log.i(TAG, "clearNameOnCreditCard: Waiting for $waitingTime ms for name on card text field to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clearNameOnCreditCard: Waited for $waitingTime ms for name on card text field to exist")
+ Log.i(TAG, "clearNameOnCreditCard: Trying to clear the name on card text field")
+ it.clearTextField()
+ Log.i(TAG, "clearNameOnCreditCard: Cleared the name on card text field")
+ }
+
+ fun clickSaveCreditCardToolbarButton() {
+ Log.i(TAG, "clickSaveCreditCardToolbarButton: Trying to click the save credit card toolbar button")
+ saveCreditCardToolbarButton().click()
+ Log.i(TAG, "clickSaveCreditCardToolbarButton: Clicked the save credit card toolbar button")
+ }
+
+ fun verifyEditCreditCardView(
+ cardNumber: String,
+ cardName: String,
+ expiryMonth: String,
+ expiryYear: String,
+ ) {
+ assertUIObjectExists(
+ editCreditCardToolbarTitle(),
+ navigateBackButton(),
+ deleteCreditCardToolbarButton(),
+ saveCreditCardToolbarButton(),
+ )
+ Log.i(TAG, "verifyEditCreditCardView: Trying to verify that the card number text field is set to: $cardNumber")
+ assertEquals(cardNumber, creditCardNumberTextInput().text)
+ Log.i(TAG, "verifyEditCreditCardView: Verified that the card number text field was set to: $cardNumber")
+ Log.i(TAG, "verifyEditCreditCardView: Trying to verify that the card name text field is set to: $cardName")
+ assertEquals(cardName, nameOnCreditCardTextInput().text)
+ Log.i(TAG, "verifyEditCreditCardView: Verified that the card card name text field was set to: $cardName")
+
+ // Can't get the text from the drop-down items, need to verify them individually
+ assertUIObjectExists(
+ expiryYearDropDown(),
+ expiryMonthDropDown(),
+ )
+
+ assertUIObjectExists(
+ itemContainingText(expiryMonth),
+ itemContainingText(expiryYear),
+ )
+
+ assertUIObjectExists(
+ saveButton(),
+ cancelButton(),
+ )
+
+ assertUIObjectExists(deleteCreditCardMenuButton())
+ }
+
+ fun verifyEditCreditCardToolbarTitle() = assertUIObjectExists(editCreditCardToolbarTitle())
+
+ fun verifyCreditCardNumberErrorMessage() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.credit_cards_number_validation_error_message_2)))
+
+ fun verifyNameOnCreditCardErrorMessage() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.credit_cards_name_on_card_validation_error_message_2)))
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBack: Clicked the device back button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun goBackToAutofillSettings(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
+ Log.i(TAG, "goBackToAutofillSettings: Trying to click the navigate up toolbar button")
+ navigateBackButton().click()
+ Log.i(TAG, "goBackToAutofillSettings: Clicked the navigate up toolbar button")
+
+ SettingsSubMenuAutofillRobot().interact()
+ return SettingsSubMenuAutofillRobot.Transition()
+ }
+
+ fun goBackToSavedCreditCards(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
+ Log.i(TAG, "goBackToSavedCreditCards: Trying to click the navigate up toolbar button")
+ navigateBackButton().click()
+ Log.i(TAG, "goBackToSavedCreditCards: Clicked the navigate up toolbar button")
+
+ SettingsSubMenuAutofillRobot().interact()
+ return SettingsSubMenuAutofillRobot.Transition()
+ }
+
+ fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goBackToBrowser: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBackToBrowser: Clicked the device back button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+fun autofillScreen(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition {
+ SettingsSubMenuAutofillRobot().interact()
+ return SettingsSubMenuAutofillRobot.Transition()
+}
+
+private fun autofillToolbarTitle() = itemContainingText(getStringResource(R.string.preferences_autofill))
+private fun addressesSectionTitle() = itemContainingText(getStringResource(R.string.preferences_addresses))
+private fun manageAddressesToolbarTitle() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/navigationToolbar")
+ .childSelector(UiSelector().text(getStringResource(R.string.addresses_manage_addresses))),
+ )
+
+private fun saveAndAutofillAddressesOption() = itemContainingText(getStringResource(R.string.preferences_addresses_save_and_autofill_addresses_2))
+private fun saveAndAutofillAddressesSummary() = itemContainingText(getStringResource(R.string.preferences_addresses_save_and_autofill_addresses_summary_2))
+private fun addAddressButton() = itemContainingText(getStringResource(R.string.preferences_addresses_add_address))
+private fun manageAddressesButton() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("android:id/title")
+ .text(getStringResource(R.string.preferences_addresses_manage_addresses)),
+ )
+
+private fun addAddressToolbarTitle() = itemContainingText(getStringResource(R.string.addresses_add_address))
+private fun editAddressToolbarTitle() = itemContainingText(getStringResource(R.string.addresses_edit_address))
+private fun toolbarCheckmarkButton() = itemWithResId("$packageName:id/save_address_button")
+private fun navigateBackButton() = itemWithDescription(getStringResource(R.string.action_bar_up_description))
+private fun nameTextInput() = itemWithResId("$packageName:id/name_input")
+private fun streetAddressTextInput() = itemWithResId("$packageName:id/street_address_input")
+private fun cityTextInput() = itemWithResId("$packageName:id/city_input")
+private fun subRegionDropDown() = itemWithResId("$packageName:id/subregion_drop_down")
+private fun zipCodeTextInput() = itemWithResId("$packageName:id/zip_input")
+private fun countryDropDown() = itemWithResId("$packageName:id/country_drop_down")
+private fun phoneTextInput() = itemWithResId("$packageName:id/phone_input")
+private fun emailTextInput() = itemWithResId("$packageName:id/email_input")
+private fun saveButton() = itemWithResId("$packageName:id/save_button")
+private fun cancelButton() = itemWithResId("$packageName:id/cancel_button")
+private fun deleteAddressButton() = itemContainingText(getStringResource(R.string.addressess_delete_address_button))
+private fun toolbarDeleteAddressButton() = itemWithResId("$packageName:id/delete_address_button")
+private fun cancelDeleteAddressButton() = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
+private fun confirmDeleteAddressButton() = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
+
+private fun creditCardsSectionTitle() = itemContainingText(getStringResource(R.string.preferences_credit_cards_2))
+private fun saveAndAutofillCreditCardsOption() = itemContainingText(getStringResource(R.string.preferences_credit_cards_save_and_autofill_cards_2))
+private fun saveAndAutofillCreditCardsSummary() = itemContainingText(getStringResource(R.string.preferences_credit_cards_save_and_autofill_cards_summary_2))
+private fun syncCreditCardsAcrossDevicesButton() = itemContainingText(getStringResource(R.string.preferences_credit_cards_sync_cards_across_devices))
+private fun addCreditCardButton() = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_credit_cards_add_credit_card_2)))
+private fun savedCreditCardsToolbarTitle() = itemContainingText(getStringResource(R.string.credit_cards_saved_cards))
+private fun editCreditCardToolbarTitle() = itemContainingText(getStringResource(R.string.credit_cards_edit_card))
+private fun manageSavedCreditCardsButton() = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_credit_cards_manage_saved_cards_2)))
+private fun creditCardNumberTextInput() = mDevice.findObject(UiSelector().resourceId("$packageName:id/card_number_input"))
+private fun nameOnCreditCardTextInput() = mDevice.findObject(UiSelector().resourceId("$packageName:id/name_on_card_input"))
+private fun expiryMonthDropDown() = mDevice.findObject(UiSelector().resourceId("$packageName:id/expiry_month_drop_down"))
+private fun expiryYearDropDown() = mDevice.findObject(UiSelector().resourceId("$packageName:id/expiry_year_drop_down"))
+private fun savedCreditCardNumber() = mDevice.findObject(UiSelector().resourceId("$packageName:id/credit_card_logo"))
+private fun deleteCreditCardToolbarButton() = mDevice.findObject(UiSelector().resourceId("$packageName:id/delete_credit_card_button"))
+private fun saveCreditCardToolbarButton() = itemWithResId("$packageName:id/save_credit_card_button")
+private fun deleteCreditCardMenuButton() = itemContainingText(getStringResource(R.string.credit_cards_delete_card_button))
+private fun confirmDeleteCreditCardButton() = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
+private fun cancelDeleteCreditCardButton() = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
+private fun securedCreditCardsLaterButton() = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
+
+private fun savedAddress(name: String) = mDevice.findObject(UiSelector().textContains(name))
+private fun subRegionOption(subRegion: String) = mDevice.findObject(UiSelector().textContains(subRegion))
+private fun countryOption(country: String) = mDevice.findObject(UiSelector().textContains(country))
+
+private fun expiryMonthOption(expiryMonth: String) = mDevice.findObject(UiSelector().textContains(expiryMonth))
+private fun expiryYearOption(expiryYear: String) = mDevice.findObject(UiSelector().textContains(expiryYear))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt
new file mode 100644
index 0000000000..e9ffe49923
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt
@@ -0,0 +1,170 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.os.Build
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.Matchers.endsWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.TestHelper.hasCousin
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings Theme sub menu.
+ */
+class SettingsSubMenuCustomizeRobot {
+
+ fun verifyThemes() {
+ Log.i(TAG, "verifyThemes: Trying to verify that the \"Light\" mode option is visible")
+ lightModeToggle()
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyThemes: Verified that the \"Light\" mode option is visible")
+ Log.i(TAG, "verifyThemes: Trying to verify that the \"Dark\" mode option is visible")
+ darkModeToggle()
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyThemes: Verified that the \"Dark\" mode option is visible")
+ Log.i(TAG, "verifyThemes: Trying to verify that the \"Follow device theme\" option is visible")
+ deviceModeToggle()
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyThemes: Verified that the \"Follow device theme\" option is visible")
+ }
+
+ fun selectDarkMode() {
+ Log.i(TAG, "selectDarkMode: Trying to click the \"Dark\" mode option")
+ darkModeToggle().click()
+ Log.i(TAG, "selectDarkMode: Clicked the \"Dark\" mode option")
+ }
+
+ fun selectLightMode() {
+ Log.i(TAG, "selectLightMode: Trying to click the \"Light\" mode option")
+ lightModeToggle().click()
+ Log.i(TAG, "selectLightMode: Clicked the \"Light\" mode option")
+ }
+
+ fun clickTopToolbarToggle() {
+ Log.i(TAG, "clickTopToolbarToggle: Trying to click the \"Top\" toolbar option")
+ topToolbarToggle().click()
+ Log.i(TAG, "clickTopToolbarToggle: Clicked the \"Top\" toolbar option")
+ }
+
+ fun clickBottomToolbarToggle() {
+ Log.i(TAG, "clickBottomToolbarToggle: Trying to click the \"Bottom\" toolbar option")
+ bottomToolbarToggle().click()
+ Log.i(TAG, "clickBottomToolbarToggle: Clicked the \"Bottom\" toolbar option")
+ }
+
+ fun verifyToolbarPositionPreference(selectedPosition: String) {
+ Log.i(TAG, "verifyToolbarPositionPreference: Trying to verify that the $selectedPosition toolbar option is checked")
+ onView(withText(selectedPosition))
+ .check(matches(hasSibling(allOf(withId(R.id.radio_button), isChecked()))))
+ Log.i(TAG, "verifyToolbarPositionPreference: Verified that the $selectedPosition toolbar option is checked")
+ }
+
+ fun clickSwipeToolbarToSwitchTabToggle() {
+ Log.i(TAG, "clickSwipeToolbarToSwitchTabToggle: Trying to click the \"Swipe toolbar sideways to switch tabs\" toggle")
+ swipeToolbarToggle().click()
+ Log.i(TAG, "clickSwipeToolbarToSwitchTabToggle: Clicked the \"Swipe toolbar sideways to switch tabs\" toggle")
+ }
+
+ fun clickPullToRefreshToggle() {
+ Log.i(TAG, "clickPullToRefreshToggle: Trying to click the \"Pull to refresh\" toggle")
+ pullToRefreshToggle().click()
+ Log.i(TAG, "clickPullToRefreshToggle: Clicked the \"Pull to refresh\" toggle")
+ }
+
+ fun verifySwipeToolbarGesturePrefState(isEnabled: Boolean) {
+ Log.i(TAG, "verifySwipeToolbarGesturePrefState: Trying to verify that the \"Swipe toolbar sideways to switch tabs\" toggle is checked: $isEnabled")
+ swipeToolbarToggle()
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (isEnabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifySwipeToolbarGesturePrefState: Verified that the \"Swipe toolbar sideways to switch tabs\" toggle is checked: $isEnabled")
+ }
+
+ fun verifyPullToRefreshGesturePrefState(isEnabled: Boolean) {
+ Log.i(TAG, "verifyPullToRefreshGesturePrefState: Trying to verify that the \"Pull to refresh\" toggle is checked: $isEnabled")
+ pullToRefreshToggle()
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (isEnabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyPullToRefreshGesturePrefState: Verified that the \"Pull to refresh\" toggle is checked: $isEnabled")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "goBack: Waited for device to be idle")
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().perform(click())
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun darkModeToggle() = onView(withText("Dark"))
+
+private fun lightModeToggle() = onView(withText("Light"))
+
+private fun topToolbarToggle() = onView(withText("Top"))
+
+private fun bottomToolbarToggle() = onView(withText("Bottom"))
+
+private fun deviceModeToggle(): ViewInteraction {
+ val followDeviceThemeText =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "Follow device theme" else "Set by Battery Saver"
+ return onView(withText(followDeviceThemeText))
+}
+
+private fun swipeToolbarToggle() =
+ onView(withText(getStringResource(R.string.preference_gestures_swipe_toolbar_switch_tabs)))
+
+private fun pullToRefreshToggle() =
+ onView(withText(getStringResource(R.string.preference_gestures_website_pull_to_refresh)))
+
+private fun goBackButton() =
+ onView(allOf(ViewMatchers.withContentDescription("Navigate up")))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt
new file mode 100644
index 0000000000..10a3757464
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt
@@ -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.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.endsWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestHelper.hasCousin
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings Data Collection sub menu.
+ */
+class SettingsSubMenuDataCollectionRobot {
+
+ fun verifyDataCollectionView(
+ isUsageAndTechnicalDataEnabled: Boolean,
+ isMarketingDataEnabled: Boolean,
+ studiesSummary: String,
+ ) {
+ assertUIObjectExists(
+ goBackButton(),
+ itemContainingText(getStringResource(R.string.preferences_data_collection)),
+ itemContainingText(getStringResource(R.string.preference_usage_data)),
+ itemContainingText(getStringResource(R.string.preferences_usage_data_description)),
+ )
+ verifyUsageAndTechnicalDataToggle(isUsageAndTechnicalDataEnabled)
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.preferences_marketing_data)),
+ itemContainingText(getStringResource(R.string.preferences_marketing_data_description2)),
+ )
+ verifyMarketingDataToggle(isMarketingDataEnabled)
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.preference_experiments_2)),
+ itemContainingText(studiesSummary),
+ )
+ }
+
+ fun verifyUsageAndTechnicalDataToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyUsageAndTechnicalDataToggle: Trying to verify that the \"Usage and technical data\" toggle is checked: $enabled")
+ onView(withText(R.string.preference_usage_data))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyUsageAndTechnicalDataToggle: Verified that the \"Usage and technical data\" toggle is checked: $enabled")
+ }
+
+ fun verifyMarketingDataToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyMarketingDataToggle: Trying to verify that the \"Marketing data\" toggle is checked: $enabled")
+ onView(withText(R.string.preferences_marketing_data))
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyMarketingDataToggle: Verified that the \"Marketing data\" toggle is checked: $enabled")
+ }
+
+ fun verifyStudiesToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyStudiesToggle: Trying to verify that the \"Studies\" toggle is checked: $enabled")
+ onView(withId(R.id.studies_switch))
+ .check(
+ matches(
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ )
+ Log.i(TAG, "verifyStudiesToggle: Verified that the \"Studies\" toggle is checked: $enabled")
+ }
+
+ fun clickUsageAndTechnicalDataToggle() {
+ Log.i(TAG, "clickUsageAndTechnicalDataToggle: Trying to click the \"Usage and technical data\" toggle")
+ itemContainingText(getStringResource(R.string.preference_usage_data)).click()
+ Log.i(TAG, "clickUsageAndTechnicalDataToggle: Clicked the \"Usage and technical data\" toggle")
+ }
+
+ fun clickMarketingDataToggle() {
+ Log.i(TAG, "clickUsageAndTechnicalDataToggle: Trying to click the \"Marketing data\" toggle")
+ itemContainingText(getStringResource(R.string.preferences_marketing_data)).click()
+ Log.i(TAG, "clickUsageAndTechnicalDataToggle: Clicked the \"Marketing data\" toggle")
+ }
+
+ fun clickStudiesOption() {
+ Log.i(TAG, "clickStudiesOption: Trying to click the \"Studies\" option")
+ itemContainingText(getStringResource(R.string.preference_experiments_2)).click()
+ Log.i(TAG, "clickStudiesOption: Clicked the \"Studies\" option")
+ }
+
+ fun clickStudiesToggle() {
+ Log.i(TAG, "clickStudiesToggle: Trying to click the \"Studies\" toggle")
+ itemWithResId("$packageName:id/studies_switch").click()
+ Log.i(TAG, "clickStudiesToggle: Clicked the \"Studies\" toggle")
+ }
+
+ fun verifyStudiesDialog() {
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/alertTitle"),
+ itemContainingText(getStringResource(R.string.studies_restart_app)),
+ )
+ Log.i(TAG, "verifyStudiesDialog: Trying to verify that the \"Studies\" dialog \"Ok\" button is visible")
+ studiesDialogOkButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyStudiesDialog: Verified that the \"Studies\" dialog \"Ok\" button is visible")
+ Log.i(TAG, "verifyStudiesDialog: Trying to verify that the \"Studies\" dialog \"Cancel\" button is visible")
+ studiesDialogCancelButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyStudiesDialog: Verified that the \"Studies\" dialog \"Cancel\" button is visible")
+ }
+
+ fun clickStudiesDialogCancelButton() {
+ Log.i(TAG, "clickStudiesDialogCancelButton: Trying to click the \"Studies\" dialog \"Cancel\" button")
+ studiesDialogCancelButton().click()
+ Log.i(TAG, "clickStudiesDialogCancelButton: Clicked the \"Studies\" dialog \"Cancel\" button")
+ }
+
+ fun clickStudiesDialogOkButton() {
+ Log.i(TAG, "clickStudiesDialogOkButton: Trying to click the \"Studies\" dialog \"Ok\" button")
+ studiesDialogOkButton().click()
+ Log.i(TAG, "clickStudiesDialogOkButton: Clicked the \"Studies\" dialog \"Ok\" button")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() = itemWithDescription("Navigate up")
+private fun studiesDialogOkButton() = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
+private fun studiesDialogCancelButton() = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataOnQuitRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataOnQuitRobot.kt
new file mode 100644
index 0000000000..af8c306925
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataOnQuitRobot.kt
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.withChild
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withResourceName
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.containsString
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.assertIsChecked
+import org.mozilla.fenix.helpers.atPosition
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.isChecked
+
+/**
+ * Implementation of Robot Pattern for the settings Delete Browsing Data On Quit sub menu.
+ */
+class SettingsSubMenuDeleteBrowsingDataOnQuitRobot {
+
+ fun verifyNavigationToolBarHeader() {
+ Log.i(TAG, "verifyNavigationToolBarHeader: Trying to verify that the \"Delete browsing data on quit\" toolbar title is visible")
+ onView(
+ allOf(
+ withId(R.id.navigationToolbar),
+ withChild(withText(R.string.preferences_delete_browsing_data_on_quit)),
+ ),
+ )
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyNavigationToolBarHeader: Verified that the \"Delete browsing data on quit\" toolbar title is visible")
+ }
+
+ fun verifyDeleteBrowsingOnQuitEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifyDeleteBrowsingOnQuitEnabled: Trying to verify that the \"Delete browsing data on quit\" toggle is checked: $enabled")
+ deleteBrowsingOnQuitButton().assertIsChecked(enabled)
+ Log.i(TAG, "verifyDeleteBrowsingOnQuitEnabled: Verified that the \"Delete browsing data on quit\" toggle is checked: $enabled")
+ }
+
+ fun verifyDeleteBrowsingOnQuitButtonSummary() {
+ Log.i(TAG, "verifyDeleteBrowsingOnQuitButtonSummary: Trying to verify that the \"Delete browsing data on quit\" option summary is visible")
+ onView(
+ withText(R.string.preference_summary_delete_browsing_data_on_quit_2),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeleteBrowsingOnQuitButtonSummary: Verified that the \"Delete browsing data on quit\" option summary is visible")
+ }
+
+ fun clickDeleteBrowsingOnQuitButtonSwitch() {
+ Log.i(TAG, "clickDeleteBrowsingOnQuitButtonSwitch: Trying to click the \"Delete browsing data on quit\" toggle")
+ onView(withResourceName("switch_widget")).click()
+ Log.i(TAG, "clickDeleteBrowsingOnQuitButtonSwitch: Clicked the \"Delete browsing data on quit\" toggle")
+ }
+
+ fun verifyAllTheCheckBoxesText() {
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Trying to verify that the \"Open tabs\" option is visible")
+ openTabsCheckbox()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Verified that the \"Open tabs\" option is visible")
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Trying to verify that the \"Browsing history\" option is visible")
+ browsingHistoryCheckbox()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Verified that the \"Browsing history\" option is visible")
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Trying to verify that the \"Cookies and site data\" option is visible")
+ cookiesAndSiteDataCheckbox()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Verified that the \"Cookies and site data\" option is visible")
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Trying to verify that the \"Cookies and site data\" option summary is visible")
+ onView(withText(R.string.preferences_delete_browsing_data_cookies_subtitle))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Verified that the \"Cookies and site data\" option summary is visible")
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Trying to verify that the \"Cached images and files\" option is visible")
+ cachedFilesCheckbox()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Verified that the \"Cached images and files\" option is visible")
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Trying to verify that the \"Cached images and files\" option summary is visible")
+ onView(withText(R.string.preferences_delete_browsing_data_cached_files_subtitle))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Verified that the \"Cached images and files\" option summary is visible")
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Trying to verify that the \"Site permissions\" option is visible")
+ sitePermissionsCheckbox()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAllTheCheckBoxesText: Verified that the \"Site permissions\" option is visible")
+ }
+
+ fun verifyAllTheCheckBoxesChecked(checked: Boolean) {
+ for (index in 2..7) {
+ Log.i(TAG, "verifyAllTheCheckBoxesChecked: Trying to verify that the the check box at position ${index - 1} is checked: $checked")
+ onView(withId(R.id.recycler_view))
+ .check(
+ matches(
+ atPosition(
+ index,
+ hasDescendant(
+ allOf(
+ withResourceName(containsString("checkbox")),
+ isChecked(checked),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyAllTheCheckBoxesChecked: Verified that the the check box at position ${index - 1} is checked: $checked")
+ }
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() = onView(withContentDescription("Navigate up"))
+
+private fun deleteBrowsingOnQuitButton() =
+ onView(withClassName(containsString("android.widget.Switch")))
+
+private fun openTabsCheckbox() =
+ onView(withText(R.string.preferences_delete_browsing_data_tabs_title_2))
+
+private fun browsingHistoryCheckbox() =
+ onView(withText(R.string.preferences_delete_browsing_data_browsing_history_title))
+
+private fun cookiesAndSiteDataCheckbox() = onView(withText(R.string.preferences_delete_browsing_data_cookies_and_site_data))
+
+private fun cachedFilesCheckbox() =
+ onView(withText(R.string.preferences_delete_browsing_data_cached_files))
+
+private fun sitePermissionsCheckbox() =
+ onView(withText(R.string.preferences_delete_browsing_data_site_permissions))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataRobot.kt
new file mode 100644
index 0000000000..44e9356600
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataRobot.kt
@@ -0,0 +1,324 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.assertIsChecked
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings Delete Browsing Data sub menu.
+ */
+class SettingsSubMenuDeleteBrowsingDataRobot {
+
+ fun verifyAllCheckBoxesAreChecked() {
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Trying to verify that the \"Open tabs\" check box is checked")
+ openTabsCheckBox().assertIsChecked(true)
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Verified that the \"Open tabs\" check box is checked")
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Trying to verify that the \"Browsing history\" check box is checked")
+ browsingHistoryCheckBox().assertIsChecked(true)
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Verified that the \"Browsing history\" check box is checked")
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Trying to verify that the \"Cookies and site data\" check box is checked")
+ cookiesAndSiteDataCheckBox().assertIsChecked(true)
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Verified that the \"Cookies and site data\" check box is checked")
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Trying to verify that the \"Cached images and files\" check box is checked")
+ cachedFilesCheckBox().assertIsChecked(true)
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Verified that the \"Cached images and files\" check box is checked")
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Trying to verify that the \"Site permissions\" check box is checked")
+ sitePermissionsCheckBox().assertIsChecked(true)
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Verified that the \"Site permissions\" check box is checked")
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Trying to verify that the \"Downloads\" check box is checked")
+ downloadsCheckBox().assertIsChecked(true)
+ Log.i(TAG, "verifyAllCheckBoxesAreChecked: Verified that the \"Downloads\" check box is checked")
+ }
+ fun verifyOpenTabsCheckBox(status: Boolean) {
+ Log.i(TAG, "verifyOpenTabsCheckBox: Trying to verify that the \"Open tabs\" check box is checked: $status")
+ openTabsCheckBox().assertIsChecked(status)
+ Log.i(TAG, "verifyOpenTabsCheckBox: Verified that the \"Open tabs\" check box is checked: $status")
+ }
+ fun verifyBrowsingHistoryDetails(status: Boolean) {
+ Log.i(TAG, "verifyBrowsingHistoryDetails: Trying to verify that the \"Browsing history\" check box is checked: $status")
+ browsingHistoryCheckBox().assertIsChecked(status)
+ Log.i(TAG, "verifyBrowsingHistoryDetails: Verified that the \"Browsing history\" check box is checked: $status")
+ }
+ fun verifyCookiesCheckBox(status: Boolean) {
+ Log.i(TAG, "verifyCookiesCheckBox: Trying to verify that the \"Cookies and site data\" check box is checked: $status")
+ cookiesAndSiteDataCheckBox().assertIsChecked(status)
+ Log.i(TAG, "verifyCookiesCheckBox: Verified that the \"Cookies and site data\" check box is checked: $status")
+ }
+ fun verifyCachedFilesCheckBox(status: Boolean) {
+ Log.i(TAG, "verifyCachedFilesCheckBox: Trying to verify that the \"Cached images and files\" check box is checked: $status")
+ cachedFilesCheckBox().assertIsChecked(status)
+ Log.i(TAG, "verifyCachedFilesCheckBox: Verified that the \"Cached images and files\" check box is checked: $status")
+ }
+ fun verifySitePermissionsCheckBox(status: Boolean) {
+ Log.i(TAG, "verifySitePermissionsCheckBox: Trying to verify that the \"Site permissions\" check box is checked: $status")
+ sitePermissionsCheckBox().assertIsChecked(status)
+ Log.i(TAG, "verifySitePermissionsCheckBox: Verified that the \"Site permissions\" check box is checked: $status")
+ }
+ fun verifyDownloadsCheckBox(status: Boolean) {
+ Log.i(TAG, "verifyDownloadsCheckBox: Trying to verify that the \"Downloads\" check box is checked: $status")
+ downloadsCheckBox().assertIsChecked(status)
+ Log.i(TAG, "verifyDownloadsCheckBox: Verified that the \"Downloads\" check box is checked: $status")
+ }
+ fun verifyOpenTabsDetails(tabNumber: String) {
+ Log.i(TAG, "verifyOpenTabsDetails: Trying to verify that the \"Open tabs\" option summary containing $tabNumber open tabs is visible")
+ openTabsDescription(tabNumber).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyOpenTabsDetails: Verified that the \"Open tabs\" option summary containing $tabNumber open tabs is visible")
+ }
+ fun verifyBrowsingHistoryDetails(addresses: String) = assertUIObjectExists(browsingHistoryDescription(addresses))
+
+ fun verifyDeleteBrowsingDataDialog() {
+ Log.i(TAG, "verifyDeleteBrowsingDataDialog: Trying to verify that the delete browsing data dialog message is visible")
+ dialogMessage().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeleteBrowsingDataDialog: Verified that the delete browsing data dialog message is visible")
+ Log.i(TAG, "verifyDeleteBrowsingDataDialog: Trying to verify that the delete browsing data dialog \"Cancel\" button is visible")
+ dialogCancelButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeleteBrowsingDataDialog: Verified that the delete browsing data dialog \"Cancel\" button is visible")
+ Log.i(TAG, "verifyDeleteBrowsingDataDialog: Trying to verify that the delete browsing data dialog \"Delete\" button is visible")
+ dialogDeleteButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDeleteBrowsingDataDialog: Verified that the delete browsing data dialog \"Delete\" button is visible")
+ }
+
+ fun switchOpenTabsCheckBox() = clickOpenTabsCheckBox()
+ fun switchBrowsingHistoryCheckBox() = clickBrowsingHistoryCheckBox()
+ fun switchCookiesCheckBox() = clickCookiesCheckBox()
+ fun switchCachedFilesCheckBox() = clickCachedFilesCheckBox()
+ fun switchSitePermissionsCheckBox() = clickSitePermissionsCheckBox()
+ fun switchDownloadsCheckBox() = clickDownloadsCheckBox()
+ fun clickDeleteBrowsingDataButton() {
+ Log.i(TAG, "clickDeleteBrowsingDataButton: Trying to click the \"Delete browsing data\" button")
+ deleteBrowsingDataButton().click()
+ Log.i(TAG, "clickDeleteBrowsingDataButton: Clicked the \"Delete browsing data\" button")
+ }
+ fun clickDialogCancelButton() {
+ Log.i(TAG, "clickDialogCancelButton: Trying to click the delete browsing data dialog \"Cancel\" button")
+ dialogCancelButton().click()
+ Log.i(TAG, "clickDialogCancelButton: Clicked the delete browsing data dialog \"Cancel\" button")
+ }
+
+ fun selectOnlyOpenTabsCheckBox() {
+ clickBrowsingHistoryCheckBox()
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Trying to verify that the \"Browsing history\" check box is not checked")
+ browsingHistoryCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Verified that the \"Browsing history\" check box is not checked")
+
+ clickCookiesCheckBox()
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Trying to verify that the \"Cookies and site data\" check box is not checked")
+ cookiesAndSiteDataCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Verified that the \"Cookies and site data\" check box is not checked")
+
+ clickCachedFilesCheckBox()
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Trying to verify that the \"Cached images and files\" check box is not checked")
+ cachedFilesCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Verified that the \"Cached images and files\" check box is not checked")
+
+ clickSitePermissionsCheckBox()
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Trying to verify that the \"Site permissions\" check box is not checked")
+ sitePermissionsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Verified that the \"Site permissions\" check box is not checked")
+
+ clickDownloadsCheckBox()
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Trying to verify that the \"Downloads\" check box is not checked")
+ downloadsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Verified that the \"Downloads\" check box is not checked")
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Trying to verify that the \"Open tabs\" check box is checked")
+ openTabsCheckBox().assertIsChecked(true)
+ Log.i(TAG, "selectOnlyOpenTabsCheckBox: Trying to verify that the \"Open tabs\" check box is checked")
+ }
+
+ fun selectOnlyBrowsingHistoryCheckBox() {
+ clickOpenTabsCheckBox()
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Trying to verify that the \"Open tabs\" check box is not checked")
+ openTabsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Verified that the \"Open tabs\" check box is not checked")
+
+ clickCookiesCheckBox()
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Trying to verify that the \"Cookies and site data\" check box is not checked")
+ cookiesAndSiteDataCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Verified that the \"Cookies and site data\" check box is not checked")
+
+ clickCachedFilesCheckBox()
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Trying to verify that the \"Cached images and files\" check box is not checked")
+ cachedFilesCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Verified that the \"Cached images and files\" check box is not checked")
+
+ clickSitePermissionsCheckBox()
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Trying to verify that the \"Site permissions\" check box is not checked")
+ sitePermissionsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Verified that the \"Site permissions\" check box is not checked")
+
+ clickDownloadsCheckBox()
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Trying to verify that the \"Downloads\" check box is not checked")
+ downloadsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Verified that the \"Downloads\" check box is not checked")
+
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Trying to verify that the \"Browsing history\" check box is checked")
+ browsingHistoryCheckBox().assertIsChecked(true)
+ Log.i(TAG, "selectOnlyBrowsingHistoryCheckBox: Verified that the \"Browsing history\" check box is checked")
+ }
+
+ fun selectOnlyCookiesCheckBox() {
+ clickOpenTabsCheckBox()
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Trying to verify that the \"Open tabs\" check box is not checked")
+ openTabsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Verified that the \"Open tabs\" check box is not checked")
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Trying to verify that the \"Cookies and site data\" check box is checked")
+ cookiesAndSiteDataCheckBox().assertIsChecked(true)
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Verified that the \"Cookies and site data\" check box is checked")
+
+ clickCachedFilesCheckBox()
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Trying to verify that the \"Cached images and files\" check box is not checked")
+ cachedFilesCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Verified that the \"Cached images and files\" check box is not checked")
+
+ clickSitePermissionsCheckBox()
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Trying to verify that the \"Site permissions\" check box is not checked")
+ sitePermissionsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Verified that the \"Site permissions\" check box is not checked")
+
+ clickDownloadsCheckBox()
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Trying to verify that the \"Downloads\" check box is not checked")
+ downloadsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Verified that the \"Downloads\" check box is not checked")
+
+ clickBrowsingHistoryCheckBox()
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Trying to verify that the \"Browsing history\" check box is not checked")
+ browsingHistoryCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCookiesCheckBox: Verified that the \"Browsing history\" check box is not checked")
+ }
+
+ fun selectOnlyCachedFilesCheckBox() {
+ clickOpenTabsCheckBox()
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Trying to verify that the \"Open tabs\" check box is not checked")
+ openTabsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Verified that the \"Open tabs\" check box is not checked")
+
+ clickBrowsingHistoryCheckBox()
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Trying to verify that the \"Browsing history\" check box is not checked")
+ browsingHistoryCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Verified that the \"Browsing history\" check box is not checked")
+
+ clickCookiesCheckBox()
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Trying to verify that the \"Cookies and site data\" check box is not checked")
+ cookiesAndSiteDataCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Verified that the \"Cookies and site data\" check box is not checked")
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Trying to verify that the \"Cached images and files\" check box is checked")
+ cachedFilesCheckBox().assertIsChecked(true)
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Verified that the \"Cached images and files\" check box is checked")
+
+ clickSitePermissionsCheckBox()
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Trying to verify that the \"Site permissions\" check box is not checked")
+ sitePermissionsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Verified that the \"Site permissions\" check box is not checked")
+
+ clickDownloadsCheckBox()
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Trying to verify that the \"Downloads\" check box is not checked")
+ downloadsCheckBox().assertIsChecked(false)
+ Log.i(TAG, "selectOnlyCachedFilesCheckBox: Verified that the \"Downloads\" check box is not checked")
+ }
+
+ fun confirmDeletionAndAssertSnackbar() {
+ Log.i(TAG, "confirmDeletionAndAssertSnackbar: Trying to click the delete browsing data dialog \"Delete\" button")
+ dialogDeleteButton().click()
+ Log.i(TAG, "confirmDeletionAndAssertSnackbar: Clicked the delete browsing data dialog \"Delete\" button")
+ assertDeleteBrowsingDataSnackbar()
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(allOf(withContentDescription("Navigate up")))
+
+private fun deleteBrowsingDataButton() = onView(withId(R.id.delete_data))
+
+private fun dialogDeleteButton() = onView(withText("Delete")).inRoot(isDialog())
+
+private fun dialogCancelButton() = onView(withText("Cancel")).inRoot(isDialog())
+
+private fun openTabsDescription(tabNumber: String) = onView(withText("$tabNumber tabs"))
+
+private fun openTabsCheckBox() = onView(allOf(withId(R.id.checkbox), hasSibling(withText("Open tabs"))))
+
+private fun browsingHistoryDescription(addresses: String) = mDevice.findObject(UiSelector().textContains("$addresses addresses"))
+
+private fun browsingHistoryCheckBox() =
+ onView(allOf(withId(R.id.checkbox), hasSibling(withText("Browsing history"))))
+
+private fun cookiesAndSiteDataCheckBox() =
+ onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cookies and site data"))))
+
+private fun cachedFilesCheckBox() =
+ onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cached images and files"))))
+
+private fun sitePermissionsCheckBox() =
+ onView(allOf(withId(R.id.checkbox), hasSibling(withText("Site permissions"))))
+
+private fun downloadsCheckBox() =
+ onView(allOf(withId(R.id.checkbox), hasSibling(withText("Downloads"))))
+
+private fun dialogMessage() =
+ onView(withText("$appName will delete the selected browsing data."))
+ .inRoot(isDialog())
+
+private fun assertDeleteBrowsingDataSnackbar() = assertUIObjectIsGone(itemWithText("Browsing data deleted"))
+private fun clickOpenTabsCheckBox() {
+ Log.i(TAG, "clickOpenTabsCheckBox: Trying to click the \"Open tabs\" check box")
+ openTabsCheckBox().click()
+ Log.i(TAG, "clickOpenTabsCheckBox: Clicked the \"Open tabs\" check box")
+}
+private fun clickBrowsingHistoryCheckBox() {
+ Log.i(TAG, "clickBrowsingHistoryCheckBox: Trying to click the \"Browsing history\" check box")
+ browsingHistoryCheckBox().click()
+ Log.i(TAG, "clickBrowsingHistoryCheckBox: Clicked the \"Browsing history\" check box")
+}
+private fun clickCookiesCheckBox() {
+ Log.i(TAG, "clickCookiesCheckBox: Trying to click the \"Cookies and site data\" check box")
+ cookiesAndSiteDataCheckBox().click()
+ Log.i(TAG, "clickCookiesCheckBox: Clicked the \"Cookies and site data\" check box")
+}
+private fun clickCachedFilesCheckBox() {
+ Log.i(TAG, "clickCachedFilesCheckBox: Trying to click the \"Cached images and files\" check box")
+ cachedFilesCheckBox().click()
+ Log.i(TAG, "clickCachedFilesCheckBox: Clicked the \"Cached images and files\" check box")
+}
+private fun clickSitePermissionsCheckBox() {
+ Log.i(TAG, "clickSitePermissionsCheckBox: Trying to click the \"Site permissions\" check box")
+ sitePermissionsCheckBox().click()
+ Log.i(TAG, "clickSitePermissionsCheckBox: Clicked the \"Site permissions\" check box")
+}
+private fun clickDownloadsCheckBox() {
+ Log.i(TAG, "clickDownloadsCheckBox: Trying to click the \"Downloads\" check box")
+ downloadsCheckBox().click()
+ Log.i(TAG, "clickDownloadsCheckBox: Clicked the \"Downloads\" check box")
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.kt
new file mode 100644
index 0000000000..904927c723
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.kt
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.containsString
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings Enhanced Tracking Protection Exceptions sub menu.
+ */
+class SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot {
+
+ fun verifyTPExceptionsDefaultView() {
+ assertUIObjectExists(
+ itemWithText("Exceptions let you disable tracking protection for selected sites."),
+ )
+ Log.i(TAG, "verifyTPExceptionsDefaultView: Trying to verify that the ETP exceptions learn more link is displayed")
+ learnMoreLink().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyTPExceptionsDefaultView: Verified that the ETP exceptions learn more link is displayed")
+ }
+
+ fun openExceptionsLearnMoreLink() {
+ Log.i(TAG, "openExceptionsLearnMoreLink: Trying to click the ETP exceptions learn more link")
+ learnMoreLink().click()
+ Log.i(TAG, "openExceptionsLearnMoreLink: Clicked the ETP exceptions learn more link")
+ }
+
+ fun removeOneSiteException(siteHost: String) {
+ Log.i(TAG, "removeOneSiteException: Waiting for $waitingTime ms for exceptions list to exist to exist")
+ exceptionsList().waitForExists(waitingTime)
+ Log.i(TAG, "removeOneSiteException: Waited for $waitingTime ms for exceptions list to exist to exist")
+ Log.i(TAG, "removeOneSiteException: Trying to click the delete site exception button")
+ removeSiteExceptionButton(siteHost).click()
+ Log.i(TAG, "removeOneSiteException: Clicked the delete site exception button")
+ }
+
+ fun verifySiteExceptionExists(siteUrl: String, shouldExist: Boolean) {
+ Log.i(TAG, "verifySiteExceptionExists: Waiting for $waitingTime ms for exceptions list to exist to exist")
+ exceptionsList().waitForExists(waitingTime)
+ Log.i(TAG, "verifySiteExceptionExists: Waited for $waitingTime ms for exceptions list to exist to exist")
+ assertUIObjectExists(itemContainingText(siteUrl), exists = shouldExist)
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): SettingsSubMenuEnhancedTrackingProtectionRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsSubMenuEnhancedTrackingProtectionRobot().interact()
+ return SettingsSubMenuEnhancedTrackingProtectionRobot.Transition()
+ }
+
+ fun disableExceptions(interact: SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.() -> Unit): Transition {
+ Log.i(TAG, "disableExceptions: Trying to click the \"Turn on for all sites\" button")
+ disableAllExceptionsButton().click()
+ Log.i(TAG, "disableExceptions: Clicked the \"Turn on for all sites\" button")
+
+ SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot().interact()
+ return Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(allOf(withContentDescription("Navigate up")))
+
+private fun learnMoreLink() = onView(withText("Learn more"))
+
+private fun disableAllExceptionsButton() = onView(withId(R.id.removeAllExceptions))
+
+private fun removeSiteExceptionButton(siteHost: String) =
+ onView(
+ allOf(
+ withContentDescription("Delete"),
+ hasSibling(withText(containsString(siteHost))),
+ ),
+ )
+
+private fun exceptionsList() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/exceptions_list"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionRobot.kt
new file mode 100644
index 0000000000..e6f4e318dc
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionRobot.kt
@@ -0,0 +1,364 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withChild
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParentIndex
+import androidx.test.espresso.matcher.ViewMatchers.withResourceName
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.isChecked
+import org.mozilla.fenix.helpers.isEnabled
+
+const val globalPrivacyControlSwitchText = "Tell websites not to share & sell data"
+
+/**
+ * Implementation of Robot Pattern for the settings Enhanced Tracking Protection sub menu.
+ */
+class SettingsSubMenuEnhancedTrackingProtectionRobot {
+
+ fun verifyEnhancedTrackingProtectionSummary() {
+ Log.i(TAG, "verifyEnhancedTrackingProtectionSummary: Trying to verify that the ETP summary is visible")
+ onView(withText("$appName protects you from many of the most common trackers that follow what you do online."))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEnhancedTrackingProtectionSummary: Verified that the ETP summary is visible")
+ }
+
+ fun verifyLearnMoreText() {
+ Log.i(TAG, "verifyLearnMoreText: Trying to verify that the learn more link is visible")
+ onView(withText("Learn more")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyLearnMoreText: Verified that the learn more link is visible")
+ }
+
+ fun verifyEnhancedTrackingProtectionTextWithSwitchWidget() {
+ Log.i(TAG, "verifyEnhancedTrackingProtectionTextWithSwitchWidget: Trying to verify that the ETP toggle is visible")
+ onView(
+ allOf(
+ withParentIndex(1),
+ withChild(withText("Enhanced Tracking Protection")),
+ ),
+ )
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEnhancedTrackingProtectionTextWithSwitchWidget: Verified that the ETP toggle is visible")
+ }
+
+ fun verifyEnhancedTrackingProtectionOptionsEnabled(enabled: Boolean = true) {
+ Log.i(TAG, "verifyEnhancedTrackingProtectionOptionsEnabled: Trying to verify that the \"Standard\" ETP option is enabled $enabled")
+ onView(withText("Standard (default)"))
+ .check(matches(isEnabled(enabled)))
+ Log.i(TAG, "verifyEnhancedTrackingProtectionOptionsEnabled: Verified that the \"Standard\" ETP option is enabled $enabled")
+ Log.i(TAG, "verifyEnhancedTrackingProtectionOptionsEnabled: Trying to verify that the \"Strict\" ETP option is enabled $enabled")
+ onView(withText("Strict"))
+ .check(matches(isEnabled(enabled)))
+ Log.i(TAG, "verifyEnhancedTrackingProtectionOptionsEnabled: Verified that the \"Strict\" ETP option is enabled $enabled")
+ Log.i(TAG, "verifyEnhancedTrackingProtectionOptionsEnabled: Trying to verify that the \"Custom\" ETP option is enabled $enabled")
+ onView(withText("Custom"))
+ .check(matches(isEnabled(enabled)))
+ Log.i(TAG, "verifyEnhancedTrackingProtectionOptionsEnabled: Verified that the \"Custom\" ETP option is enabled $enabled")
+ }
+
+ fun verifyTrackingProtectionSwitchEnabled() {
+ Log.i(TAG, "verifyTrackingProtectionSwitchEnabled: Trying to verify that the ETP toggle is checked")
+ onView(withResourceName("checkbox")).check(
+ matches(
+ isChecked(
+ true,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyTrackingProtectionSwitchEnabled: Verified that the ETP toggle is checked")
+ }
+
+ fun switchEnhancedTrackingProtectionToggle() {
+ Log.i(TAG, "switchEnhancedTrackingProtectionToggle: Trying to click the ETP toggle")
+ onView(
+ allOf(
+ withText("Enhanced Tracking Protection"),
+ hasSibling(withResourceName("checkbox")),
+ ),
+ ).click()
+ Log.i(TAG, "switchEnhancedTrackingProtectionToggle: Clicked the ETP toggle")
+ }
+
+ fun scrollToGCPSettings() {
+ Log.i(TAG, "scrollToGCPSettings: Trying to perform scroll to the $globalPrivacyControlSwitchText option")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(globalPrivacyControlSwitchText)),
+ ),
+ )
+ Log.i(TAG, "scrollToGCPSettings: Performed scroll to the $globalPrivacyControlSwitchText option")
+ }
+ fun verifyGPCTextWithSwitchWidget() {
+ Log.i(TAG, "verifyGPCTextWithSwitchWidget: Trying to verify that the $globalPrivacyControlSwitchText option is visible")
+ onView(
+ allOf(
+ withChild(withText(globalPrivacyControlSwitchText)),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyGPCTextWithSwitchWidget: Verified that the $globalPrivacyControlSwitchText option is visible")
+ }
+
+ fun verifyGPCSwitchEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifyGPCSwitchEnabled: Trying to verify that the $globalPrivacyControlSwitchText option is checked: $enabled")
+ onView(
+ allOf(
+ withChild(withText(globalPrivacyControlSwitchText)),
+ ),
+ ).check(matches(isChecked(enabled)))
+ Log.i(TAG, "verifyGPCSwitchEnabled: Verified that the $globalPrivacyControlSwitchText option is checked: $enabled")
+ }
+
+ fun switchGPCToggle() {
+ Log.i(TAG, "switchGPCToggle: Trying to click the $globalPrivacyControlSwitchText option toggle")
+ onView(
+ allOf(
+ withChild(withText(globalPrivacyControlSwitchText)),
+ ),
+ ).click()
+ Log.i(TAG, "switchGPCToggle: Clicked the $globalPrivacyControlSwitchText option toggle")
+ }
+
+ fun verifyStandardOptionDescription() {
+ Log.i(TAG, "verifyStandardOptionDescription: Trying to verify that the \"Standard\" ETP option summary is displayed")
+ onView(withText(R.string.preference_enhanced_tracking_protection_standard_description_5))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyStandardOptionDescription: Verified that the \"Standard\" ETP option summary is displayed")
+ Log.i(TAG, "verifyStandardOptionDescription: Trying to verify that the \"Standard\" ETP option info button is displayed")
+ onView(withContentDescription(R.string.preference_enhanced_tracking_protection_standard_info_button))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyStandardOptionDescription: Verify that the \"Standard\" ETP option info button is displayed")
+ }
+
+ fun verifyStrictOptionDescription() {
+ Log.i(TAG, "verifyStrictOptionDescription: Trying to verify that the \"Strict\" ETP option summary is displayed")
+ onView(withText(R.string.preference_enhanced_tracking_protection_strict_description_4))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyStrictOptionDescription: Verified that the \"Strict\" ETP option summary is displayed")
+ Log.i(TAG, "verifyStrictOptionDescription: Trying to verify that the \"Strict\" ETP option info button is displayed")
+ onView(withContentDescription(R.string.preference_enhanced_tracking_protection_strict_info_button))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyStrictOptionDescription: Verified that the \"Strict\" ETP option info button is displayed")
+ }
+
+ fun verifyCustomTrackingProtectionSettings() {
+ scrollToElementByText("Redirect Trackers")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Custom\" ETP option summary is displayed")
+ onView(withText(R.string.preference_enhanced_tracking_protection_custom_description_2))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Custom\" ETP option summary is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Custom\" ETP option info button is displayed")
+ onView(withContentDescription(R.string.preference_enhanced_tracking_protection_custom_info_button))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Custom\" ETP option info button is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Cookies\" check box is displayed")
+ cookiesCheckbox().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Cookies\" check box is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Cookies\" drop down is displayed")
+ cookiesDropDownMenuDefault().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Cookies\" drop down is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Tracking content\" check box is displayed")
+ trackingContentCheckbox().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Tracking content\" check box is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Tracking content\" drop down is displayed")
+ trackingcontentDropDownDefault().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Tracking content\" drop down is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Cryptominers\" check box is displayed")
+ cryptominersCheckbox().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Cryptominers\" check box is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Fingerprinters\" check box is displayed")
+ fingerprintersCheckbox().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Fingerprinters\" check box is displayed")
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Trying to verify that the \"Redirect trackers\" check box is displayed")
+ redirectTrackersCheckbox().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCustomTrackingProtectionSettings: Verified that the \"Redirect trackers\" check box is displayed")
+ }
+
+ fun verifyWhatsBlockedByStandardETPInfo() {
+ Log.i(TAG, "verifyWhatsBlockedByStandardETPInfo: Trying to click the \"Standard\" ETP option info button")
+ onView(withContentDescription(R.string.preference_enhanced_tracking_protection_standard_info_button)).click()
+ Log.i(TAG, "verifyWhatsBlockedByStandardETPInfo: Clicked the \"Standard\" ETP option info button")
+ blockedByStandardETPInfo()
+ }
+
+ fun verifyWhatsBlockedByStrictETPInfo() {
+ Log.i(TAG, "verifyWhatsBlockedByStrictETPInfo: Trying to click the \"Strict\" ETP option info button")
+ onView(withContentDescription(R.string.preference_enhanced_tracking_protection_strict_info_button)).click()
+ Log.i(TAG, "verifyWhatsBlockedByStrictETPInfo: Clicked the \"Strict\" ETP option info button")
+ // Repeating the info as in the standard option, with one extra point.
+ blockedByStandardETPInfo()
+ Log.i(TAG, "verifyWhatsBlockedByStrictETPInfo: Trying to verify that the \"Tracking Content\" title is displayed")
+ onView(withText("Tracking Content")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyWhatsBlockedByStrictETPInfo: Verified that the \"Tracking Content\" title is displayed")
+ Log.i(TAG, "verifyWhatsBlockedByStrictETPInfo: Trying to verify that the \"Tracking Content\" summary is displayed")
+ onView(withText("Stops outside ads, videos, and other content from loading that contains tracking code. May affect some website functionality.")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyWhatsBlockedByStrictETPInfo: Verified that the \"Tracking Content\" summary is displayed")
+ }
+
+ fun verifyWhatsBlockedByCustomETPInfo() {
+ Log.i(TAG, "verifyWhatsBlockedByCustomETPInfo: Trying to click the \"Custom\" ETP option info button")
+ onView(withContentDescription(R.string.preference_enhanced_tracking_protection_custom_info_button)).click()
+ Log.i(TAG, "verifyWhatsBlockedByCustomETPInfo: Clicked the \"Custom\" ETP option info button")
+ // Repeating the info as in the standard option, with one extra point.
+ blockedByStandardETPInfo()
+ Log.i(TAG, "verifyWhatsBlockedByCustomETPInfo: Trying to verify that the \"Tracking Content\" title is displayed")
+ onView(withText("Tracking Content")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyWhatsBlockedByCustomETPInfo: Verified that the \"Tracking Content\" title is displayed")
+ Log.i(TAG, "verifyWhatsBlockedByCustomETPInfo: Trying to verify that the \"Tracking Content\" summary is displayed")
+ onView(withText("Stops outside ads, videos, and other content from loading that contains tracking code. May affect some website functionality.")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyWhatsBlockedByCustomETPInfo: Verified that the \"Tracking Content\" summary is displayed")
+ }
+
+ fun selectTrackingProtectionOption(option: String) {
+ Log.i(TAG, "selectTrackingProtectionOption: Trying to click the $option ETP option")
+ onView(withText(option)).click()
+ Log.i(TAG, "selectTrackingProtectionOption: Clicked the $option ETP option")
+ }
+
+ fun verifyEnhancedTrackingProtectionLevelSelected(option: String, checked: Boolean) {
+ Log.i(TAG, "verifyEnhancedTrackingProtectionLevelSelected: Waiting for $waitingTime ms until finding the \"Enhanced Tracking Protection\" toolbar")
+ mDevice.wait(
+ Until.findObject(By.text("Enhanced Tracking Protection")),
+ waitingTime,
+ )
+ Log.i(TAG, "verifyEnhancedTrackingProtectionLevelSelected: Waited for $waitingTime ms until the \"Enhanced Tracking Protection\" toolbar was found")
+ Log.i(TAG, "verifyEnhancedTrackingProtectionLevelSelected: Trying to verify that the $option ETP option is checked: $checked")
+ onView(withText(option))
+ .check(
+ matches(
+ hasSibling(
+ allOf(
+ withId(R.id.radio_button),
+ isChecked(checked),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyEnhancedTrackingProtectionLevelSelected: Verified that the $option ETP option is checked: $checked")
+ }
+
+ class Transition {
+ fun goBackToHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ // To settings
+ Log.i(TAG, "goBackToHomeScreen: Trying to click the navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBackToHomeScreen: Clicked the navigate up toolbar button")
+ // To HomeScreen
+ Log.i(TAG, "goBackToHomeScreen: Trying to perform press back action")
+ pressBack()
+ Log.i(TAG, "goBackToHomeScreen: Performed press back action")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun openExceptions(
+ interact: SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.() -> Unit,
+ ): SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.Transition {
+ Log.i(TAG, "openExceptions: Trying to perform scroll to the \"Exceptions\" option")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Exceptions")),
+ ),
+ )
+ Log.i(TAG, "openExceptions: Performed scroll to the \"Exceptions\" option")
+ Log.i(TAG, "openExceptions: Trying to click the \"Exceptions\" option")
+ openExceptions().click()
+ Log.i(TAG, "openExceptions: Clicked the \"Exceptions\" option")
+
+ SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot().interact()
+ return SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.Transition()
+ }
+ }
+}
+
+fun settingsSubMenuEnhancedTrackingProtection(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): SettingsSubMenuEnhancedTrackingProtectionRobot.Transition {
+ SettingsSubMenuEnhancedTrackingProtectionRobot().interact()
+ return SettingsSubMenuEnhancedTrackingProtectionRobot.Transition()
+}
+
+private fun goBackButton() =
+ onView(allOf(withContentDescription("Navigate up")))
+
+private fun openExceptions() =
+ onView(allOf(withText("Exceptions")))
+
+private fun cookiesCheckbox() = onView(withText("Cookies"))
+
+private fun cookiesDropDownMenuDefault() = onView(withText("Isolate cross-site cookies"))
+
+private fun trackingContentCheckbox() = onView(withText("Tracking content"))
+
+private fun trackingcontentDropDownDefault() = onView(withText("In all tabs"))
+
+private fun cryptominersCheckbox() = onView(withText("Cryptominers"))
+
+private fun fingerprintersCheckbox() = onView(withText("Fingerprinters"))
+
+private fun redirectTrackersCheckbox() = onView(withText("Redirect Trackers"))
+
+private fun blockedByStandardETPInfo() {
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Social Media Trackers\" title is displayed")
+ onView(withText("Social Media Trackers")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Social Media Trackers\" title is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Social Media Trackers\" summary is displayed")
+ onView(withText("Limits the ability of social networks to track your browsing activity around the web.")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Social Media Trackers\" summary is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Cross-Site Cookies\" title is displayed")
+ onView(withText("Cross-Site Cookies")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Cross-Site Cookies\" title is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Cross-Site Cookies\" summary is displayed")
+ onView(withText("Total Cookie Protection isolates cookies to the site you’re on so trackers like ad networks can’t use them to follow you across sites.")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Cross-Site Cookies\" summary is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Cryptominers\" title is displayed")
+ onView(withText("Cryptominers")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Cryptominers\" title is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Cryptominers\" summary is displayed")
+ onView(withText("Prevents malicious scripts gaining access to your device to mine digital currency.")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Cryptominers\" summary is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Fingerprinters\" title is displayed")
+ onView(withText("Fingerprinters")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Fingerprinters\" title is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Fingerprinters\" summary is displayed")
+ onView(withText("Stops uniquely identifiable data from being collected about your device that can be used for tracking purposes.")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Fingerprinters\" summary is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Redirect Trackers\" title is displayed")
+ onView(withText("Redirect Trackers")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Redirect Trackers\" title is displayed")
+ Log.i(TAG, "blockedByStandardETPInfo: Trying to verify that the \"Redirect Trackers\" summary is displayed")
+ onView(withText("Clears cookies set by redirects to known tracking websites.")).check(matches(isDisplayed()))
+ Log.i(TAG, "blockedByStandardETPInfo: Verified that the \"Redirect Trackers\" summary is displayed")
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuExperimentsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuExperimentsRobot.kt
new file mode 100644
index 0000000000..6326cb8f9d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuExperimentsRobot.kt
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.uiautomator.UiSelector
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the experiments sub menu.
+ */
+class SettingsSubMenuExperimentsRobot {
+
+ class Transition {
+
+ fun goBackToHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ goBackButton().click()
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ goBackButton().click()
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+
+ private fun getExperiment(experimentName: String): UiSelector? {
+ return UiSelector().textContains(experimentName)
+ }
+
+ fun verifyExperimentExists(title: String) {
+ val experiment = getExperiment(title)
+
+ checkNotNull(experiment)
+ }
+
+ fun verifyExperimentEnrolled(title: String) {
+ itemContainingText(title).click()
+ assertUIObjectExists(checkIcon())
+ goBackButton().click()
+ }
+
+ fun verifyExperimentNotEnrolled(title: String) {
+ itemContainingText(title).click()
+ assertUIObjectExists(checkIcon(), exists = false)
+ goBackButton().click()
+ }
+
+ fun unenrollfromExperiment(title: String) {
+ val branch = itemWithResId("$packageName:id/nimbus_branch_name")
+
+ itemContainingText(title).click()
+ assertUIObjectExists(checkIcon())
+ branch.click()
+ assertUIObjectExists(checkIcon(), exists = false)
+ }
+}
+private fun goBackButton() = onView(withContentDescription(R.string.action_bar_up_description))
+private fun checkIcon() = itemWithResId("$packageName:id/selected_icon")
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHomepageRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHomepageRobot.kt
new file mode 100644
index 0000000000..9d1c919a37
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHomepageRobot.kt
@@ -0,0 +1,489 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.Matchers
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.isChecked
+
+/**
+ * Implementation of Robot Pattern for the settings Homepage sub menu.
+ */
+class SettingsSubMenuHomepageRobot {
+
+ fun verifyHomePageView(
+ shortcutsSwitchEnabled: Boolean = true,
+ sponsoredShortcutsCheckBox: Boolean = true,
+ jumpBackInSwitchEnabled: Boolean = true,
+ recentBookmarksSwitchEnabled: Boolean = true,
+ recentlyVisitedSwitchEnabled: Boolean = true,
+ pocketSwitchEnabled: Boolean = true,
+ sponsoredStoriesCheckBox: Boolean = true,
+ ) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Shortcuts\" option is visible")
+ shortcutsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Shortcuts\" option is visible")
+ if (shortcutsSwitchEnabled) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Shortcuts\" toggle is checked")
+ shortcutsButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Shortcuts\" toggle is checked")
+ } else {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Shortcuts\" toggle is not checked")
+ shortcutsButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Shortcuts\" toggle is not checked")
+ }
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Sponsored shortcuts\" option is visible")
+ sponsoredShortcutsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Sponsored shortcuts\" option is visible")
+ if (sponsoredShortcutsCheckBox) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Sponsored shortcuts\" check box is checked")
+ sponsoredShortcutsButton()
+ .check(
+ matches(
+ hasSibling(
+ ViewMatchers.withChild(
+ allOf(
+ withClassName(CoreMatchers.endsWith("CheckBox")),
+ isChecked(),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Sponsored shortcuts\" check box is checked")
+ } else {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Sponsored shortcuts\" check box is not checked")
+ sponsoredShortcutsButton()
+ .check(
+ matches(
+ hasSibling(
+ ViewMatchers.withChild(
+ allOf(
+ withClassName(CoreMatchers.endsWith("CheckBox")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Sponsored shortcuts\" check box is not checked")
+ }
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Jump back in\" option is visible")
+ jumpBackInButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Jump back in\" option is visible")
+ if (jumpBackInSwitchEnabled) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Jump back in\" toggle is checked")
+ jumpBackInButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Jump back in\" toggle is checked")
+ } else {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Jump back in\" toggle is not checked")
+ jumpBackInButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Jump back in\" toggle is not checked")
+ }
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Recent bookmarks\" option is visible")
+ recentBookmarksButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Recent bookmarks\" option is visible")
+ if (recentBookmarksSwitchEnabled) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Recent bookmarks\" toggle is checked")
+ recentBookmarksButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Recent bookmarks\" toggle is checked")
+ } else {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Recent bookmarks\" toggle is not checked")
+ recentBookmarksButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Recent bookmarks\" toggle is not checked")
+ }
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Recently visited\" option is visible")
+ recentlyVisitedButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Recently visited\" option is visible")
+ if (recentlyVisitedSwitchEnabled) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Recently visited\" toggle is checked")
+ recentlyVisitedButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Recently visited\" toggle is checked")
+ } else {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Recently visited\" toggle is not checked")
+ recentlyVisitedButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Recently visited\" toggle is not checked")
+ }
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Thought-provoking stories\" option is visible")
+ pocketButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Thought-provoking stories\" option is visible")
+ if (pocketSwitchEnabled) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Thought-provoking stories\" toggle is checked")
+ pocketButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Thought-provoking stories\" toggle is checked")
+ } else {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Thought-provoking stories\" toggle is not checked")
+ pocketButton()
+ .check(
+ matches(
+ TestHelper.hasCousin(
+ Matchers.allOf(
+ withClassName(Matchers.endsWith("Switch")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Thought-provoking stories\" toggle is not checked")
+ }
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Sponsored stories\" option is visible")
+ sponsoredStoriesButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Sponsored stories\" option is visible")
+ if (sponsoredStoriesCheckBox) {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Sponsored stories\" check box is checked")
+ sponsoredStoriesButton()
+ .check(
+ matches(
+ hasSibling(
+ ViewMatchers.withChild(
+ allOf(
+ withClassName(CoreMatchers.endsWith("CheckBox")),
+ isChecked(),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Sponsored stories\" check box is checked")
+ } else {
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Sponsored stories\" check box is not checked")
+ sponsoredStoriesButton()
+ .check(
+ matches(
+ hasSibling(
+ ViewMatchers.withChild(
+ allOf(
+ withClassName(CoreMatchers.endsWith("CheckBox")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Sponsored stories\" check box is not checked")
+ }
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Opening screen\" heading is visible")
+ openingScreenHeading().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Opening screen\" heading is visible")
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Homepage\" option is visible")
+ homepageButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Homepage\" option is visible")
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Last tab\" option is visible")
+ lastTabButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Last tab\" option is visible")
+ Log.i(TAG, "verifyHomePageView: Trying to verify that the \"Homepage after four hours of inactivity\" option is visible")
+ homepageAfterFourHoursButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyHomePageView: Verified that the \"Homepage after four hours of inactivity\" option is visible")
+ }
+
+ fun verifySelectedOpeningScreenOption(openingScreenOption: String) {
+ Log.i(TAG, "verifySelectedOpeningScreenOption: Trying to verify that the \"Opening screen\" option $openingScreenOption is checked")
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(openingScreenOption)),
+ ),
+ ).check(matches(isChecked(true)))
+ Log.i(TAG, "verifySelectedOpeningScreenOption: Verified that the \"Opening screen\" option $openingScreenOption is checked")
+ }
+
+ fun clickShortcutsButton() {
+ Log.i(TAG, "clickShortcutsButton: Trying to click the \"Shortcuts\" option")
+ shortcutsButton().click()
+ Log.i(TAG, "clickShortcutsButton: Clicked the \"Shortcuts\" option")
+ }
+
+ fun clickSponsoredShortcuts() {
+ Log.i(TAG, "clickSponsoredShortcuts: Trying to click the \"Sponsored shortcuts\" option")
+ sponsoredShortcutsButton().click()
+ Log.i(TAG, "clickSponsoredShortcuts: Clicked the \"Sponsored shortcuts\" option")
+ }
+
+ fun clickJumpBackInButton() {
+ Log.i(TAG, "clickJumpBackInButton: Trying to click the \"Jump back in\" option")
+ jumpBackInButton().click()
+ Log.i(TAG, "clickJumpBackInButton: Clicked the \"Jump back in\" option")
+ }
+
+ fun clickRecentlyVisited() {
+ Log.i(TAG, "clickRecentlyVisited: Trying to click the \"Recently visited\" option")
+ recentlyVisitedButton().click()
+ Log.i(TAG, "clickRecentlyVisited: Clicked the \"Recently visited\" option")
+ }
+
+ fun clickRecentBookmarksButton() {
+ Log.i(TAG, "clickRecentBookmarksButton: Trying to click the \"Recent bookmarks\" option")
+ recentBookmarksButton().click()
+ Log.i(TAG, "clickRecentBookmarksButton: Clicked the \"Recent bookmarks\" option")
+ }
+
+ fun clickRecentSearchesButton() {
+ Log.i(TAG, "clickRecentSearchesButton: Trying to click the \"Recently visited\" option")
+ recentlyVisitedButton().click()
+ Log.i(TAG, "clickRecentSearchesButton: Clicked the \"Recently visited\" option")
+ }
+
+ fun clickPocketButton() {
+ Log.i(TAG, "clickPocketButton: Trying to click the \"Thought-provoking stories\" option")
+ pocketButton().click()
+ Log.i(TAG, "clickPocketButton: Clicked the \"Thought-provoking stories\" option")
+ }
+
+ fun clickOpeningScreenOption(openingScreenOption: String) {
+ Log.i(TAG, "clickOpeningScreenOption: Trying to click \"Opening screen\" option: $openingScreenOption")
+ when (openingScreenOption) {
+ "Homepage" -> homepageButton().click()
+ "Last tab" -> lastTabButton().click()
+ "Homepage after four hours of inactivity" -> homepageAfterFourHoursButton().click()
+ }
+ Log.i(TAG, "clickOpeningScreenOption: Clicked \"Opening screen\" option: $openingScreenOption")
+ }
+
+ fun openWallpapersMenu() {
+ Log.i(TAG, "openWallpapersMenu: Trying to click the \"Wallpapers\" option")
+ wallpapersMenuButton().click()
+ Log.i(TAG, "openWallpapersMenu: Clicked the \"Wallpapers\" option")
+ }
+
+ fun selectWallpaper(wallpaperName: String) {
+ Log.i(TAG, "selectWallpaper: Trying to click wallpaper: $wallpaperName")
+ mDevice.findObject(UiSelector().description(wallpaperName)).click()
+ Log.i(TAG, "selectWallpaper: Clicked wallpaper: $wallpaperName")
+ }
+
+ fun verifySponsoredShortcutsCheckBox(checked: Boolean) {
+ if (checked) {
+ Log.i(TAG, "verifySponsoredShortcutsCheckBox: Trying to verify that the \"Sponsored shortcuts\" check box is checked")
+ sponsoredShortcutsButton()
+ .check(
+ matches(
+ hasSibling(
+ ViewMatchers.withChild(
+ allOf(
+ withClassName(CoreMatchers.endsWith("CheckBox")),
+ isChecked(),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifySponsoredShortcutsCheckBox: Verified that the \"Sponsored shortcuts\" check box is checked")
+ } else {
+ Log.i(TAG, "verifySponsoredShortcutsCheckBox: Trying to verify that the \"Sponsored shortcuts\" check box is not checked")
+ sponsoredShortcutsButton()
+ .check(
+ matches(
+ hasSibling(
+ ViewMatchers.withChild(
+ allOf(
+ withClassName(CoreMatchers.endsWith("CheckBox")),
+ isNotChecked(),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifySponsoredShortcutsCheckBox: Verified that the \"Sponsored shortcuts\" check box is not checked")
+ }
+ }
+
+ class Transition {
+
+ fun goBackToHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "goBackToHomeScreen: Trying to click the navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBackToHomeScreen: Clicked the navigate up toolbar button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun clickSnackBarViewButton(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "clickSnackBarViewButton: Waiting for $waitingTimeShort ms for \"VIEW\" snackbar button to exist")
+ mDevice.findObject(UiSelector().text("VIEW")).waitForExists(waitingTimeShort)
+ Log.i(TAG, "clickSnackBarViewButton: Waited for $waitingTimeShort ms for \"VIEW\" snackbar button to exist")
+ Log.i(TAG, "clickSnackBarViewButton: Trying to click the \"VIEW\" snackbar button")
+ mDevice.findObject(UiSelector().text("VIEW")).click()
+ Log.i(TAG, "clickSnackBarViewButton: Clicked the \"VIEW\" snackbar button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+ }
+}
+
+private fun shortcutsButton() =
+ onView(allOf(withText(R.string.top_sites_toggle_top_recent_sites_4)))
+
+private fun sponsoredShortcutsButton() =
+ onView(allOf(withText(R.string.customize_toggle_contile)))
+
+private fun jumpBackInButton() =
+ onView(allOf(withText(R.string.customize_toggle_jump_back_in)))
+
+private fun recentBookmarksButton() =
+ onView(allOf(withText(R.string.customize_toggle_recent_bookmarks)))
+
+private fun recentlyVisitedButton() =
+ onView(allOf(withText(R.string.customize_toggle_recently_visited)))
+
+private fun pocketButton() =
+ onView(allOf(withText(R.string.customize_toggle_pocket_2)))
+
+private fun sponsoredStoriesButton() =
+ onView(allOf(withText(R.string.customize_toggle_pocket_sponsored)))
+
+private fun openingScreenHeading() = onView(withText(R.string.preferences_opening_screen))
+
+private fun homepageButton() =
+ onView(
+ allOf(
+ withId(R.id.title),
+ withText(R.string.opening_screen_homepage),
+ hasSibling(withId(R.id.radio_button)),
+ ),
+ )
+
+private fun lastTabButton() =
+ onView(
+ allOf(
+ withId(R.id.title),
+ withText(R.string.opening_screen_last_tab),
+ hasSibling(withId(R.id.radio_button)),
+ ),
+ )
+
+private fun homepageAfterFourHoursButton() =
+ onView(
+ allOf(
+ withId(R.id.title),
+ withText(R.string.opening_screen_after_four_hours_of_inactivity),
+ hasSibling(withId(R.id.radio_button)),
+ ),
+ )
+
+private fun goBackButton() = onView(allOf(withContentDescription(R.string.action_bar_up_description)))
+
+private fun wallpapersMenuButton() = onView(withText("Wallpapers"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHttpsOnlyModeRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHttpsOnlyModeRobot.kt
new file mode 100644
index 0000000000..d4a0c5c642
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHttpsOnlyModeRobot.kt
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.assertIsChecked
+import org.mozilla.fenix.helpers.assertIsEnabled
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.isChecked
+
+class SettingsSubMenuHttpsOnlyModeRobot {
+
+ fun verifyHttpsOnlyModeMenuHeader() {
+ Log.i(TAG, "verifyHttpsOnlyModeMenuHeader: Trying to verify that the \"HTTPS-Only Mode\" toolbar items are visible")
+ onView(
+ allOf(
+ withText(getStringResource(R.string.preferences_https_only_title)),
+ hasSibling(withContentDescription("Navigate up")),
+ ),
+ ).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyHttpsOnlyModeMenuHeader: Verified that the \"HTTPS-Only Mode\" toolbar items are visible")
+ }
+
+ fun verifyHttpsOnlyModeSummary() {
+ Log.i(TAG, "verifyHttpsOnlyModeSummary: Trying to verify that the \"HTTPS-Only Mode\" title is visible")
+ onView(withId(R.id.https_only_title))
+ .check(matches(withText("HTTPS-Only Mode")))
+ Log.i(TAG, "verifyHttpsOnlyModeSummary: Verified that the \"HTTPS-Only Mode\" title is visible")
+ Log.i(TAG, "verifyHttpsOnlyModeSummary: Trying to verify that the \"HTTPS-Only Mode\" summary is visible")
+ onView(withId(R.id.https_only_summary))
+ .check(matches(withText("Automatically attempts to connect to sites using HTTPS encryption protocol for increased security. Learn more")))
+ Log.i(TAG, "verifyHttpsOnlyModeSummary: Verified that the \"HTTPS-Only Mode\" summary is visible")
+ }
+
+ fun verifyHttpsOnlyModeIsEnabled(shouldBeEnabled: Boolean) {
+ Log.i(TAG, "verifyHttpsOnlyModeIsEnabled: Trying to verify that the \"HTTPS-Only Mode\" toggle is checked: $shouldBeEnabled")
+ httpsModeOnlySwitch().check(
+ matches(
+ if (shouldBeEnabled) {
+ isChecked(true)
+ } else {
+ isChecked(false)
+ },
+ ),
+ )
+ Log.i(TAG, "verifyHttpsOnlyModeIsEnabled: Verified that the \"HTTPS-Only Mode\" toggle is checked: $shouldBeEnabled")
+ }
+
+ fun clickHttpsOnlyModeSwitch() {
+ Log.i(TAG, "clickHttpsOnlyModeSwitch: Trying to click the \"HTTPS-Only Mode\" toggle")
+ httpsModeOnlySwitch().click()
+ Log.i(TAG, "clickHttpsOnlyModeSwitch: Clicked the \"HTTPS-Only Mode\" toggle")
+ }
+
+ fun verifyHttpsOnlyModeOptionsEnabled(shouldBeEnabled: Boolean) {
+ Log.i(TAG, "verifyHttpsOnlyModeOptionsEnabled: Trying to verify that the \"Enable in all tabs\" option is enabled: $shouldBeEnabled")
+ allTabsOption().assertIsEnabled(shouldBeEnabled)
+ Log.i(TAG, "verifyHttpsOnlyModeOptionsEnabled: Verified that the \"Enable in all tabs\" option is enabled: $shouldBeEnabled")
+ Log.i(TAG, "verifyHttpsOnlyModeOptionsEnabled: Trying to verify that the \"Enable only in private tabs\" option is enabled: $shouldBeEnabled")
+ onlyPrivateTabsOption().assertIsEnabled(shouldBeEnabled)
+ Log.i(TAG, "verifyHttpsOnlyModeOptionsEnabled: Verified that the \"Enable only in private tabs\" option is enabled: $shouldBeEnabled")
+ }
+
+ fun verifyHttpsOnlyOptionSelected(allTabsOptionSelected: Boolean, privateTabsOptionSelected: Boolean) {
+ if (allTabsOptionSelected) {
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Trying to verify that the \"Enable in all tabs\" option is checked: $allTabsOptionSelected")
+ allTabsOption().assertIsChecked(true)
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Verified that the \"Enable in all tabs\" option is checked: $allTabsOptionSelected")
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Trying to verify that the \"Enable only in private tabs\" option is checked: $privateTabsOptionSelected")
+ onlyPrivateTabsOption().assertIsChecked(false)
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Verified that the \"Enable only in private tabs\" option is checked: $privateTabsOptionSelected")
+ } else if (privateTabsOptionSelected) {
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Trying to verify that the \"Enable in all tabs\" option is checked: $allTabsOptionSelected")
+ allTabsOption().assertIsChecked(false)
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Verified that the \"Enable in all tabs\" option is checked: $allTabsOptionSelected")
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Trying to verify that the \"Enable only in private tabs\" option is checked: $privateTabsOptionSelected")
+ onlyPrivateTabsOption().assertIsChecked(true)
+ Log.i(TAG, "verifyHttpsOnlyOptionSelected: Verified that the \"Enable only in private tabs\" option is checked: $privateTabsOptionSelected")
+ }
+ }
+
+ fun selectHttpsOnlyModeOption(allTabsOptionSelected: Boolean, privateTabsOptionSelected: Boolean) {
+ if (allTabsOptionSelected) {
+ Log.i(TAG, "selectHttpsOnlyModeOption: Trying to click the \"Enable in all tabs\" option")
+ allTabsOption().click()
+ Log.i(TAG, "selectHttpsOnlyModeOption: Clicked the \"Enable in all tabs\" option")
+ Log.i(TAG, "selectHttpsOnlyModeOption: Trying to verify that the \"Enable in all tabs\" option is checked: $allTabsOptionSelected")
+ allTabsOption().assertIsChecked(true)
+ Log.i(TAG, "selectHttpsOnlyModeOption: Verified that the \"Enable in all tabs\" option is checked: $allTabsOptionSelected")
+ } else if (privateTabsOptionSelected) {
+ Log.i(TAG, "selectHttpsOnlyModeOption: Trying to click the \"Enable only in private tabs\" option")
+ onlyPrivateTabsOption().click()
+ Log.i(TAG, "selectHttpsOnlyModeOption: Clicked the \"Enable only in private tabs\" option")
+ Log.i(TAG, "selectHttpsOnlyModeOption: Trying to verify that the \"Enable only in private tabs\" option is checked: $privateTabsOptionSelected")
+ onlyPrivateTabsOption().assertIsChecked(true)
+ Log.i(TAG, "selectHttpsOnlyModeOption: Verified that the \"Enable only in private tabs\" option is checked: $privateTabsOptionSelected")
+ }
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up toolbar button")
+ goBackButton().perform(click())
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun httpsModeOnlySwitch() = onView(withId(R.id.https_only_switch))
+
+private fun allTabsOption() =
+ onView(
+ allOf(
+ withId(R.id.https_only_all_tabs),
+ withText("Enable in all tabs"),
+ ),
+ )
+
+private fun onlyPrivateTabsOption() =
+ onView(
+ allOf(
+ withId(R.id.https_only_private_tabs),
+ withText("Enable only in private tabs"),
+ ),
+ )
+
+private fun goBackButton() = onView(withContentDescription("Navigate up"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLanguageRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLanguageRobot.kt
new file mode 100644
index 0000000000..c288c13ca1
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLanguageRobot.kt
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+class SettingsSubMenuLanguageRobot {
+ fun selectLanguage(language: String) {
+ Log.i(TAG, "selectLanguage: Waiting for $waitingTime ms for language list to exist")
+ languagesList().waitForExists(waitingTime)
+ Log.i(TAG, "selectLanguage: Waited for $waitingTime ms for language list to exist")
+ Log.i(TAG, "selectLanguage: Trying to click language: $language")
+ languagesList()
+ .getChildByText(UiSelector().text(language), language)
+ .click()
+ Log.i(TAG, "selectLanguage: Clicked language: $language")
+ }
+
+ fun selectLanguageSearchResult(languageName: String) {
+ Log.i(TAG, "selectLanguageSearchResult: Waiting for $waitingTime ms for language list to exist")
+ language(languageName).waitForExists(waitingTime)
+ Log.i(TAG, "selectLanguageSearchResult: Waited for $waitingTime ms for language list to exist")
+ Log.i(TAG, "selectLanguageSearchResult: Trying to click language: $languageName")
+ language(languageName).click()
+ Log.i(TAG, "selectLanguageSearchResult: Clicked language: $languageName")
+ }
+
+ fun verifyLanguageHeaderIsTranslated(translation: String) = assertUIObjectExists(itemWithText(translation))
+
+ fun verifySelectedLanguage(language: String) {
+ Log.i(TAG, "verifySelectedLanguage: Waiting for $waitingTime ms for language list to exist")
+ languagesList().waitForExists(waitingTime)
+ Log.i(TAG, "verifySelectedLanguage: Waited for $waitingTime ms for language list to exist")
+ assertUIObjectExists(
+ languagesList()
+ .getChildByText(UiSelector().text(language), language, true)
+ .getFromParent(UiSelector().resourceId("$packageName:id/locale_selected_icon")),
+ )
+ }
+
+ fun openSearchBar() {
+ Log.i(TAG, "openSearchBar: Trying to click the search bar")
+ onView(withId(R.id.search)).click()
+ Log.i(TAG, "openSearchBar: Clicked the search bar")
+ }
+
+ fun typeInSearchBar(text: String) {
+ Log.i(TAG, "typeInSearchBar: Waiting for $waitingTime ms for search bar to exist")
+ searchBar().waitForExists(waitingTime)
+ Log.i(TAG, "typeInSearchBar: Waited for $waitingTime ms for search bar to exist")
+ Log.i(TAG, "typeInSearchBar: Trying to set search bar text to: $text")
+ searchBar().setText(text)
+ Log.i(TAG, "typeInSearchBar: Search bar text was set to: $text")
+ }
+
+ fun verifySearchResultsContains(languageName: String) =
+ assertUIObjectExists(language(languageName))
+
+ fun clearSearchBar() {
+ Log.i(TAG, "clearSearchBar: Trying to click the clear search bar button")
+ onView(withId(R.id.search_close_btn)).click()
+ Log.i(TAG, "clearSearchBar: Clicked the clear search bar button")
+ }
+
+ fun verifyLanguageListIsDisplayed() = assertUIObjectExists(languagesList())
+
+ class Transition {
+
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "goBack: Waited for device to be idle")
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(ViewActions.click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
+
+private fun languagesList() =
+ UiScrollable(
+ UiSelector()
+ .resourceId("$packageName:id/locale_list")
+ .scrollable(true),
+ )
+
+private fun language(name: String) = mDevice.findObject(UiSelector().text(name))
+
+private fun searchBar() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/search_src_text"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.kt
new file mode 100644
index 0000000000..f107104f9d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.kt
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.not
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+
+/**
+ * Implementation of Robot Pattern for the Privacy Settings > saved logins sub menu
+ */
+
+class SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot {
+ fun verifySaveLoginsOptionsView() {
+ Log.i(TAG, "verifySaveLoginsOptionsView: Trying to verify that the \"Ask to save\" option is visible")
+ onView(withText("Ask to save"))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifySaveLoginsOptionsView: Verified that the \"Ask to save\" option is visible")
+ Log.i(TAG, "verifySaveLoginsOptionsView: Trying to verify that the \"Never save\" option is visible")
+ onView(withText("Never save"))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifySaveLoginsOptionsView: Verified that the \"Never save\" option is visible")
+ }
+
+ fun verifyAskToSaveRadioButton(isChecked: Boolean) {
+ if (isChecked) {
+ Log.i(TAG, "verifyAskToSaveRadioButton: Trying to verify that the \"Ask to save\" option is checked")
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(R.string.preferences_passwords_save_logins_ask_to_save)),
+ ),
+ ).check(matches(isChecked()))
+ Log.i(TAG, "verifyAskToSaveRadioButton: Verified that the \"Ask to save\" option is checked")
+ } else {
+ Log.i(TAG, "verifyAskToSaveRadioButton: Trying to verify that the \"Ask to save\" option is not checked")
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(R.string.preferences_passwords_save_logins_ask_to_save)),
+ ),
+ ).check(matches(not(isChecked())))
+ Log.i(TAG, "verifyAskToSaveRadioButton: Verified that the \"Ask to save\" option is not checked")
+ }
+ }
+
+ fun verifyNeverSaveSaveRadioButton(isChecked: Boolean) {
+ if (isChecked) {
+ Log.i(TAG, "verifyNeverSaveSaveRadioButton: Trying to verify that the \"Never save\" option is checked")
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(R.string.preferences_passwords_save_logins_never_save)),
+ ),
+ ).check(matches(isChecked()))
+ Log.i(TAG, "verifyNeverSaveSaveRadioButton: Verified that the \"Never save\" option is checked")
+ } else {
+ Log.i(TAG, "verifyNeverSaveSaveRadioButton: Trying to verify that the \"Never save\" option is not checked")
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(R.string.preferences_passwords_save_logins_never_save)),
+ ),
+ ).check(matches(not(isChecked())))
+ Log.i(TAG, "verifyNeverSaveSaveRadioButton: Verified that the \"Never save\" option is not checked")
+ }
+ }
+
+ fun clickNeverSaveOption() {
+ Log.i(TAG, "clickNeverSaveOption: Trying to click the \"Never save\" option")
+ itemContainingText(getStringResource(R.string.preferences_passwords_save_logins_never_save)).click()
+ Log.i(TAG, "clickNeverSaveOption: Clicked the \"Never save\" option")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(ViewActions.click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsSubMenuLoginsAndPasswordRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(allOf(ViewMatchers.withContentDescription("Navigate up")))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordRobot.kt
new file mode 100644
index 0000000000..29eb7ae5cd
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordRobot.kt
@@ -0,0 +1,195 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.content.Context
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.endsWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestAssetHelper
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.hasCousin
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+/**
+ * Implementation of Robot Pattern for the Privacy Settings > logins and passwords sub menu
+ */
+class SettingsSubMenuLoginsAndPasswordRobot {
+
+ fun verifyDefaultView() {
+ mDevice.waitNotNull(Until.findObjects(By.text("Save passwords")), TestAssetHelper.waitingTime)
+ Log.i(TAG, "verifyDefaultView: Trying to verify that the \"Save logins and passwords\" button is displayed")
+ saveLoginsAndPasswordButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultView: Verified that the \"Save passwords\" button is displayed")
+ Log.i(TAG, "verifyDefaultView: Trying to verify that the Autofill in Firefox option is displayed")
+ autofillInFirefoxOption().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultView: Verified that the Autofill in Firefox option is displayed")
+ Log.i(TAG, "verifyDefaultView: Trying to verify that the \"Autofill in other apps\" option is displayed")
+ autofillInOtherAppsOption().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultView: Verified that the \"Autofill in other apps\" option is displayed")
+ Log.i(TAG, "verifyDefaultView: Trying to verify that the \"Sync logins across devices\" button is displayed")
+ syncLoginsButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultView: Verified that the \"Sync logins across devices\" button is displayed")
+ Log.i(TAG, "verifyDefaultView: Trying to verify that the \"Saved logins\" button is displayed")
+ savedLoginsButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultView: Verified that the \"Saved logins\" button is displayed")
+ Log.i(TAG, "verifyDefaultView: Trying to verify that the \"Exceptions\" button is displayed")
+ loginExceptionsButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultView: Verified that the \"Exceptions\" button is displayed")
+ }
+
+ fun verifyDefaultViewBeforeSyncComplete() {
+ mDevice.waitNotNull(Until.findObjects(By.text("Off")), TestAssetHelper.waitingTime)
+ }
+
+ fun verifyDefaultViewAfterSync() {
+ mDevice.waitNotNull(Until.findObjects(By.text("On")), TestAssetHelper.waitingTime)
+ }
+
+ fun verifyDefaultValueAutofillLogins(context: Context) {
+ Log.i(TAG, "verifyDefaultValueAutofillLogins: Trying to verify that the Autofill in Firefox option is displayed")
+ onView(
+ withText(
+ context.getString(
+ R.string.preferences_passwords_autofill2,
+ context.getString(R.string.app_name),
+ ),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDefaultValueAutofillLogins: Verified that the Autofill in Firefox option is displayed")
+ }
+
+ fun clickAutofillInFirefoxOption() {
+ Log.i(TAG, "clickAutofillInFirefoxOption: Trying to click the Autofill in Firefox option")
+ autofillInFirefoxOption().click()
+ Log.i(TAG, "clickAutofillInFirefoxOption: Clicked the Autofill in Firefox option")
+ }
+
+ fun verifyAutofillInFirefoxToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyAutofillInFirefoxToggle: Trying to verify that the Autofill in Firefox toggle is enabled: $enabled")
+ autofillInFirefoxOption()
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withClassName(endsWith("Switch")),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyAutofillInFirefoxToggle: Verified that the Autofill in Firefox toggle is enabled: $enabled")
+ }
+ fun verifyAutofillLoginsInOtherAppsToggle(enabled: Boolean) {
+ Log.i(TAG, "verifyAutofillLoginsInOtherAppsToggle: Trying to verify that the \"Autofill in other apps\" toggle is enabled: $enabled")
+ autofillInOtherAppsOption()
+ .check(
+ matches(
+ hasCousin(
+ allOf(
+ withId(R.id.switch_widget),
+ if (enabled) {
+ isChecked()
+ } else {
+ isNotChecked()
+ },
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyAutofillLoginsInOtherAppsToggle: Verified that the \"Autofill in other apps\" toggle is enabled: $enabled")
+ }
+
+ class Transition {
+
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun openSavedLogins(interact: SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition {
+ Log.i(TAG, "openSavedLogins: Trying to click the \"Saved logins\" button")
+ savedLoginsButton().click()
+ Log.i(TAG, "openSavedLogins: Clicked the \"Saved logins\" button")
+
+ SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
+ }
+
+ fun openLoginExceptions(interact: SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition {
+ Log.i(TAG, "openLoginExceptions: Trying to click the \"Exceptions\" button")
+ loginExceptionsButton().click()
+ Log.i(TAG, "openLoginExceptions: Clicked the \"Exceptions\" button")
+
+ SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
+ }
+
+ fun openSyncLogins(interact: SettingsTurnOnSyncRobot.() -> Unit): SettingsTurnOnSyncRobot.Transition {
+ Log.i(TAG, "openSyncLogins: Trying to click the \"Sync logins across devices\" button")
+ syncLoginsButton().click()
+ Log.i(TAG, "openSyncLogins: Clicked the \"Sync logins across devices\" button")
+
+ SettingsTurnOnSyncRobot().interact()
+ return SettingsTurnOnSyncRobot.Transition()
+ }
+
+ fun openSaveLoginsAndPasswordsOptions(interact: SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.Transition {
+ Log.i(TAG, "openSaveLoginsAndPasswordsOptions: Trying to click the \"Save logins and passwords\" button")
+ saveLoginsAndPasswordButton().click()
+ Log.i(TAG, "openSaveLoginsAndPasswordsOptions: Clicked the \"Save logins and passwords\" button")
+
+ SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.Transition()
+ }
+ }
+}
+
+fun settingsSubMenuLoginsAndPassword(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
+ SettingsSubMenuLoginsAndPasswordRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordRobot.Transition()
+}
+
+private fun saveLoginsAndPasswordButton() = onView(withText("Save passwords"))
+
+private fun savedLoginsButton() = onView(withText("Saved passwords"))
+
+private fun syncLoginsButton() = onView(withText("Sync passwords across devices"))
+
+private fun loginExceptionsButton() = onView(withText("Exceptions"))
+
+private fun goBackButton() =
+ onView(allOf(ViewMatchers.withContentDescription("Navigate up")))
+
+private fun autofillInFirefoxOption() = onView(withText("Autofill in $appName"))
+
+private fun autofillInOtherAppsOption() = onView(withText("Autofill in other apps"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.kt
new file mode 100644
index 0000000000..3bd0c8081e
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.kt
@@ -0,0 +1,343 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withHint
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.CoreMatchers
+import org.hamcrest.CoreMatchers.containsString
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
+import org.mozilla.fenix.helpers.MatcherHelper.assertItemIsEnabledAndVisible
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithClassNameAndIndex
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+/**
+ * Implementation of Robot Pattern for the Privacy Settings > saved logins sub menu
+ */
+
+class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
+ fun verifySecurityPromptForLogins() {
+ Log.i(TAG, "verifySecurityPromptForLogins: Trying to verify that the \"Secure your saved passwords\" dialog is visible")
+ onView(withText("Secure your saved passwords")).check(
+ matches(
+ withEffectiveVisibility(
+ ViewMatchers.Visibility.VISIBLE,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifySecurityPromptForLogins: Verified that the \"Secure your saved passwords\" dialog is visible")
+ }
+
+ fun verifyEmptySavedLoginsListView() {
+ Log.i(TAG, "verifyEmptySavedLoginsListView: Trying to verify that the saved logins section description is displayed")
+ onView(withText(getStringResource(R.string.preferences_passwords_saved_logins_description_empty_text_2)))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyEmptySavedLoginsListView: Verified that the saved logins section description is displayed")
+ Log.i(TAG, "verifyEmptySavedLoginsListView: Trying to verify that the \"Learn more about Sync\" link is displayed")
+ onView(withText(R.string.preferences_passwords_saved_logins_description_empty_learn_more_link_2))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyEmptySavedLoginsListView: Verified that the \"Learn more about Sync\" link is displayed")
+ Log.i(TAG, "verifyEmptySavedLoginsListView: Trying to verify that the \"Add login\" button is displayed")
+ onView(withText(R.string.preferences_logins_add_login_2))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyEmptySavedLoginsListView: Verified that the \"Add login\" button is displayed")
+ }
+
+ fun verifySavedLoginsAfterSync() {
+ mDevice.waitNotNull(
+ Until.findObjects(By.text("https://accounts.google.com")),
+ waitingTime,
+ )
+ Log.i(TAG, "verifySavedLoginsAfterSync: Trying to verify that the \"https://accounts.google.comn\" login is displayed")
+ onView(withText("https://accounts.google.com")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifySavedLoginsAfterSync: Verified that the \"https://accounts.google.comn\" login is displayed")
+ }
+
+ fun tapSetupLater() {
+ Log.i(TAG, "tapSetupLater: Trying to click the \"Later\" dialog button")
+ onView(withText("Later")).perform(ViewActions.click())
+ Log.i(TAG, "tapSetupLater: Clicked the \"Later\" dialog button")
+ }
+
+ fun clickAddLoginButton() {
+ Log.i(TAG, "clickAddLoginButton: Trying to click the \"Add login\" button")
+ itemContainingText(getStringResource(R.string.preferences_logins_add_login_2)).click()
+ Log.i(TAG, "clickAddLoginButton: Clicked the \"Add login\" button")
+ }
+
+ fun verifyAddNewLoginView() {
+ assertUIObjectExists(
+ siteHeader(),
+ siteTextInput(),
+ usernameHeader(),
+ usernameTextInput(),
+ passwordHeader(),
+ passwordTextInput(),
+ siteDescription(),
+ )
+ Log.i(TAG, "verifyAddNewLoginView: Trying to verify the \"https://www.example.com\" site text box hint")
+ siteTextInputHint().check(matches(withHint(R.string.add_login_hostname_hint_text)))
+ Log.i(TAG, "verifyAddNewLoginView: Verified the \"https://www.example.com\" site text box hint")
+ }
+
+ fun enterSiteCredential(website: String) {
+ Log.i(TAG, "enterSiteCredential: Trying to set the \"Site\" text box text to: $website")
+ siteTextInput().setText(website)
+ Log.i(TAG, "enterSiteCredential: The \"Site\" text box text was set to: $website")
+ }
+
+ fun verifyHostnameErrorMessage() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.add_login_hostname_invalid_text_2)))
+
+ fun verifyPasswordErrorMessage() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_login_password_required_2)))
+
+ fun verifyPasswordClearButtonEnabled() =
+ assertItemIsEnabledAndVisible(itemWithResId("$packageName:id/clearPasswordTextButton"))
+
+ fun verifyHostnameClearButtonEnabled() =
+ assertItemIsEnabledAndVisible(itemWithResId("$packageName:id/clearHostnameTextButton"))
+
+ fun clickSearchLoginButton() {
+ Log.i(TAG, "clickSearchLoginButton: Trying to click the search logins button")
+ itemWithResId("$packageName:id/search").click()
+ Log.i(TAG, "clickSearchLoginButton: Clicked the search logins button")
+ }
+
+ fun clickSavedLoginsChevronIcon() {
+ Log.i(TAG, "clickSavedLoginsChevronIcon: Trying to click the \"Saved logins\" chevron button")
+ itemWithResId("$packageName:id/toolbar_chevron_icon").click()
+ Log.i(TAG, "clickSavedLoginsChevronIcon: Clicked the \"Saved logins\" chevron button")
+ }
+
+ fun verifyLoginsSortingOptions() {
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_alphabetically)))
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_last_used)))
+ }
+
+ fun clickLastUsedSortingOption() {
+ Log.i(TAG, "clickLastUsedSortingOption: Trying to click the \"Last used\" sorting option")
+ itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_last_used)).click()
+ Log.i(TAG, "clickLastUsedSortingOption: Clicked the \"Last used\" sorting option")
+ }
+
+ fun verifySortedLogin(position: Int, loginTitle: String) =
+ assertUIObjectExists(
+ itemWithClassNameAndIndex(className = "android.view.ViewGroup", index = position)
+ .getChild(
+ UiSelector()
+ .resourceId("$packageName:id/webAddressView")
+ .textContains(loginTitle),
+ ),
+ )
+
+ fun searchLogin(searchTerm: String) {
+ Log.i(TAG, "searchLogin: Trying to set the search bar text to: $searchTerm")
+ itemWithResId("$packageName:id/search").setText(searchTerm)
+ Log.i(TAG, "searchLogin: Search bar text was set to: $searchTerm")
+ }
+
+ fun verifySavedLoginsSectionUsername(username: String) =
+ mDevice.waitNotNull(Until.findObjects(By.text(username)))
+
+ fun verifyLoginItemUsername(username: String) = assertUIObjectExists(itemContainingText(username))
+
+ fun verifyNotSavedLoginFromPrompt() {
+ Log.i(TAG, "verifyNotSavedLoginFromPrompt: Trying to verify that \"test@example.com\" does not exist in the saved logins list")
+ onView(withText("test@example.com"))
+ .check(ViewAssertions.doesNotExist())
+ Log.i(TAG, "verifyNotSavedLoginFromPrompt: Verified that \"test@example.com\" does not exist in the saved logins list")
+ }
+
+ fun verifyLocalhostExceptionAdded() {
+ Log.i(TAG, "verifyLocalhostExceptionAdded: Trying to verify that \"localhost\" is visible in the exceptions list")
+ onView(withText(containsString("localhost")))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyLocalhostExceptionAdded: Verified that \"localhost\" is visible in the exceptions list")
+ }
+
+ fun viewSavedLoginDetails(loginUserName: String) {
+ Log.i(TAG, "viewSavedLoginDetails: Trying to click $loginUserName saved login")
+ onView(withText(loginUserName)).click()
+ Log.i(TAG, "viewSavedLoginDetails: Clicked $loginUserName saved login")
+ }
+
+ fun clickThreeDotButton(activityTestRule: HomeActivityIntentTestRule) {
+ Log.i(TAG, "clickThreeDotButton: Trying to click the three dot button")
+ openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
+ Log.i(TAG, "clickThreeDotButton: Clicked the three dot button")
+ }
+
+ fun clickEditLoginButton() {
+ Log.i(TAG, "clickEditLoginButton: Trying to click the \"Edit\" button")
+ itemContainingText("Edit").click()
+ Log.i(TAG, "clickEditLoginButton: Clicked the \"Edit\" button")
+ }
+
+ fun clickDeleteLoginButton() {
+ Log.i(TAG, "clickDeleteLoginButton: Trying to click the \"Delete\" button")
+ itemContainingText("Delete").click()
+ Log.i(TAG, "clickDeleteLoginButton: Clicked the \"Delete\" button")
+ }
+
+ fun verifyLoginDeletionPrompt() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.login_deletion_confirmation_2)))
+
+ fun clickConfirmDeleteLogin() {
+ Log.i(TAG, "clickConfirmDeleteLogin: Trying to click the \"Delete\" dialog button")
+ onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog()).click()
+ Log.i(TAG, "clickConfirmDeleteLogin: Clicked the \"Delete\" dialog button")
+ }
+
+ fun clickCancelDeleteLogin() {
+ Log.i(TAG, "clickCancelDeleteLogin: Trying to click the \"Cancel\" dialog button")
+ onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog()).click()
+ Log.i(TAG, "clickCancelDeleteLogin: Clicked the \"Cancel\" dialog button")
+ }
+
+ fun setNewUserName(userName: String) {
+ Log.i(TAG, "setNewUserName: Trying to set \"Username\" text box to: $userName")
+ usernameTextInput().setText(userName)
+ Log.i(TAG, "setNewUserName: \"Username\" text box was set to: $userName")
+ }
+
+ fun clickClearUserNameButton() {
+ Log.i(TAG, "clickClearUserNameButton: Trying to click the clear username button")
+ itemWithResId("$packageName:id/clearUsernameTextButton").click()
+ Log.i(TAG, "clickClearUserNameButton: Clicked the clear username button")
+ }
+
+ fun setNewPassword(password: String) {
+ Log.i(TAG, "setNewPassword: Trying to set \"Password\" text box to: $password")
+ passwordTextInput().setText(password)
+ Log.i(TAG, "setNewPassword: \"Password\" text box was set to: $password")
+ }
+
+ fun clickClearPasswordButton() {
+ Log.i(TAG, "clickClearPasswordButton: Trying to click the clear password button")
+ itemWithResId("$packageName:id/clearPasswordTextButton").click()
+ Log.i(TAG, "clickClearPasswordButton: Clicked the clear password button")
+ }
+
+ fun saveEditedLogin() {
+ Log.i(TAG, "saveEditedLogin: Trying to click the toolbar save button")
+ itemWithResId("$packageName:id/save_login_button").click()
+ Log.i(TAG, "saveEditedLogin: Clicked the toolbar save button")
+ }
+
+ fun verifySaveLoginButtonIsEnabled(isEnabled: Boolean) =
+ assertUIObjectExists(
+ checkedItemWithResId("$packageName:id/save_login_button", isChecked = true),
+ exists = isEnabled,
+ )
+
+ fun revealPassword() {
+ Log.i(TAG, "revealPassword: Trying to click the reveal password button")
+ onView(withId(R.id.revealPasswordButton)).click()
+ Log.i(TAG, "revealPassword: Clicked the reveal password button")
+ }
+
+ fun verifyPasswordSaved(password: String) {
+ Log.i(TAG, "verifyPasswordSaved: Trying to verify that the \"Password\" text box is set to $password")
+ onView(withId(R.id.passwordText)).check(matches(withText(password)))
+ Log.i(TAG, "verifyPasswordSaved: Verified that the \"Password\" text box is set to $password")
+ }
+
+ fun verifyUserNameRequiredErrorMessage() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_login_username_required_2)))
+
+ fun verifyPasswordRequiredErrorMessage() =
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_login_password_required_2)))
+
+ fun clickGoBackButton() = goBackButton().click()
+
+ fun clickCopyUserNameButton() =
+ itemWithResId("$packageName:id/copyUsername").also {
+ Log.i(TAG, "clickCopyUserNameButton: Waiting for $waitingTime ms for the copy username button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickCopyUserNameButton: Waited for $waitingTime ms for the copy username button to exist")
+ Log.i(TAG, "clickCopyUserNameButton:Trying to click the copy username button")
+ it.click()
+ Log.i(TAG, "clickCopyUserNameButton:Clicked the copy username button")
+ }
+
+ fun clickCopyPasswordButton() =
+ itemWithResId("$packageName:id/copyPassword").also {
+ Log.i(TAG, "clickCopyPasswordButton: Waiting for $waitingTime ms for the copy password button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickCopyPasswordButton: Waited for $waitingTime ms for the copy password button to exist")
+ Log.i(TAG, "clickCopyPasswordButton:Trying to click the copy password button")
+ it.click()
+ Log.i(TAG, "clickCopyPasswordButton:Clicked the copy password button")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(ViewActions.click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsSubMenuLoginsAndPasswordRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordRobot.Transition()
+ }
+
+ fun goBackToSavedLogins(interact: SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition {
+ Log.i(TAG, "goBackToSavedLogins: Trying to click the navigate up button")
+ goBackButton().perform(ViewActions.click())
+ Log.i(TAG, "goBackToSavedLogins: Clicked the navigate up button")
+
+ SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot().interact()
+ return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
+ }
+
+ fun goToSavedWebsite(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goToSavedWebsite: Trying to click the open web site button")
+ openWebsiteButton().click()
+ Log.i(TAG, "goToSavedWebsite: Clicked the open web site button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
+
+private fun openWebsiteButton() = onView(withId(R.id.openWebAddress))
+
+private fun siteHeader() = itemWithResId("$packageName:id/hostnameHeaderText")
+private fun siteTextInput() = itemWithResId("$packageName:id/hostnameText")
+private fun siteDescription() = itemContainingText(getStringResource(R.string.add_login_hostname_invalid_text_3))
+private fun siteTextInputHint() = onView(withId(R.id.hostnameText))
+private fun usernameHeader() = itemWithResId("$packageName:id/usernameHeader")
+private fun usernameTextInput() = itemWithResId("$packageName:id/usernameText")
+private fun passwordHeader() = itemWithResId("$packageName:id/passwordHeader")
+private fun passwordTextInput() = itemWithResId("$packageName:id/passwordText")
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuOpenLinksInAppsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuOpenLinksInAppsRobot.kt
new file mode 100644
index 0000000000..1ee72b2cd7
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuOpenLinksInAppsRobot.kt
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.isChecked
+
+/**
+ * Implementation of Robot Pattern for the Open Links In Apps sub menu.
+ */
+class SettingsSubMenuOpenLinksInAppsRobot {
+
+ fun verifyOpenLinksInAppsView(selectedOpenLinkInAppsOption: String) {
+ assertUIObjectExists(
+ goBackButton(),
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps)),
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_always)),
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_ask)),
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_never)),
+ )
+ verifySelectedOpenLinksInAppOption(selectedOpenLinkInAppsOption)
+ }
+
+ fun verifyPrivateOpenLinksInAppsView(selectedOpenLinkInAppsOption: String) {
+ assertUIObjectExists(
+ goBackButton(),
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps)),
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_ask)),
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_never)),
+ )
+ verifySelectedOpenLinksInAppOption(selectedOpenLinkInAppsOption)
+ }
+
+ fun verifySelectedOpenLinksInAppOption(openLinkInAppsOption: String) {
+ Log.i(TAG, "verifySelectedOpenLinksInAppOption: Trying to verify that the $openLinkInAppsOption option is checked")
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(openLinkInAppsOption)),
+ ),
+ ).check(matches(isChecked(true)))
+ Log.i(TAG, "verifySelectedOpenLinksInAppOption: Verified that the $openLinkInAppsOption option is checked")
+ }
+
+ fun clickOpenLinkInAppOption(openLinkInAppsOption: String) {
+ Log.i(TAG, "clickOpenLinkInAppOption: Trying to click the $openLinkInAppsOption option")
+ when (openLinkInAppsOption) {
+ "Always" -> alwaysOption().click()
+ "Ask before opening" -> askBeforeOpeningOption().click()
+ "Never" -> neverOption().click()
+ }
+ Log.i(TAG, "clickOpenLinkInAppOption: Clicked the $openLinkInAppsOption option")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "goBack: Waited for device to be idle")
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+private fun goBackButton() = itemWithDescription("Navigate up")
+private fun alwaysOption() =
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_always))
+private fun askBeforeOpeningOption() =
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_ask))
+private fun neverOption() =
+ itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_never))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt
new file mode 100644
index 0000000000..62bde104e7
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.os.Build
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.By.text
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.appName
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.isEnabled
+
+/**
+ * Implementation of Robot Pattern for the settings PrivateBrowsing sub menu.
+ */
+
+class SettingsSubMenuPrivateBrowsingRobot {
+
+ fun verifyOpenLinksInPrivateTab() {
+ Log.i(TAG, "verifyOpenLinksInPrivateTab: Trying to verify that the \"Open links in a private tab\" option is visible")
+ openLinksInPrivateTabSwitch()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyOpenLinksInPrivateTab: Verified that the \"Open links in a private tab\" option is visible")
+ }
+
+ fun verifyAddPrivateBrowsingShortcutButton() {
+ Log.i(TAG, "verifyAddPrivateBrowsingShortcutButton: Waiting for $waitingTime ms until finding the \"Add private browsing shortcut\" button")
+ mDevice.wait(
+ Until.findObject(text("Add private browsing shortcut")),
+ waitingTime,
+ )
+ Log.i(TAG, "verifyAddPrivateBrowsingShortcutButton: Waited for $waitingTime ms until the \"Add private browsing shortcut\" button was found")
+ Log.i(TAG, "verifyAddPrivateBrowsingShortcutButton: Trying to verify that the \"Add private browsing shortcut\" button is visible")
+ addPrivateBrowsingShortcutButton()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyAddPrivateBrowsingShortcutButton: Verified that the \"Add private browsing shortcut\" button is visible")
+ }
+
+ fun verifyOpenLinksInPrivateTabEnabled() {
+ Log.i(TAG, "verifyOpenLinksInPrivateTabEnabled: Trying to verify that the \"Open links in a private tab\" toggle is enabled")
+ openLinksInPrivateTabSwitch().check(matches(isEnabled(true)))
+ Log.i(TAG, "verifyOpenLinksInPrivateTabEnabled: Verified that the \"Open links in a private tab\" toggle is enabled")
+ }
+
+ fun verifyOpenLinksInPrivateTabOff() {
+ assertUIObjectExists(
+ checkedItemWithResId("android:id/switch_widget", isChecked = true),
+ exists = false,
+ )
+ Log.i(TAG, "verifyOpenLinksInPrivateTabOff: Trying to verify that the \"Open links in a private tab\" toggle is visible")
+ openLinksInPrivateTabSwitch()
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyOpenLinksInPrivateTabOff: Verified that the \"Open links in a private tab\" toggle is visible")
+ }
+
+ fun verifyPrivateBrowsingShortcutIcon() {
+ Log.i(TAG, "verifyPrivateBrowsingShortcutIcon: Waiting for $waitingTime ms until finding the \"Private $appName\" shortcut icon")
+ mDevice.wait(Until.findObject(text("Private $appName")), waitingTime)
+ Log.i(TAG, "verifyPrivateBrowsingShortcutIcon: Waited for $waitingTime ms until the \"Private $appName\" shortcut icon was found")
+ Log.i(TAG, "verifyPrivateBrowsingShortcutIcon: Trying to verify the \"Private $appName\" shortcut icon")
+ assertTrue("\"Private $appName\" shortcut icon wasn't verified", mDevice.hasObject(text("Private $appName")))
+ Log.i(TAG, "verifyPrivateBrowsingShortcutIcon: Verified the \"Private $appName\" shortcut icon")
+ }
+
+ fun clickPrivateModeScreenshotsSwitch() {
+ Log.i(TAG, "clickPrivateModeScreenshotsSwitch: Trying to click the \"Allow screenshots in private browsing\" toggle")
+ screenshotsInPrivateModeSwitch().click()
+ Log.i(TAG, "clickPrivateModeScreenshotsSwitch: Clicked the \"Allow screenshots in private browsing\" toggle")
+ }
+
+ fun clickOpenLinksInPrivateTabSwitch() {
+ Log.i(TAG, "clickOpenLinksInPrivateTabSwitch: Trying to click the \"Open links in a private tab\" toggle")
+ openLinksInPrivateTabSwitch().click()
+ Log.i(TAG, "clickOpenLinksInPrivateTabSwitch: Clicked the \"Open links in a private tab\" toggle")
+ }
+
+ fun cancelPrivateShortcutAddition() {
+ Log.i(TAG, "cancelPrivateShortcutAddition: Waiting for $waitingTime ms until finding the \"Add private browsing shortcut\" button")
+ mDevice.wait(
+ Until.findObject(text("Add private browsing shortcut")),
+ waitingTime,
+ )
+ Log.i(TAG, "cancelPrivateShortcutAddition: Waited for $waitingTime ms until the \"Add private browsing shortcut\" button was found")
+ Log.i(TAG, "cancelPrivateShortcutAddition: Trying to click the \"Add private browsing shortcut\" button")
+ addPrivateBrowsingShortcutButton().click()
+ Log.i(TAG, "cancelPrivateShortcutAddition: Clicked the \"Add private browsing shortcut\" button")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Log.i(TAG, "cancelPrivateShortcutAddition: Waiting for $waitingTime ms until finding the \"Cancel\" button")
+ mDevice.wait(Until.findObject(By.textContains("CANCEL")), waitingTime)
+ Log.i(TAG, "cancelPrivateShortcutAddition: Waited for $waitingTime ms until the \"Cancel\" button was found")
+ Log.i(TAG, "cancelPrivateShortcutAddition: Trying to click the \"Cancel\" button")
+ cancelShortcutAdditionButton().click()
+ Log.i(TAG, "cancelPrivateShortcutAddition: Clicked the \"Cancel\" button")
+ }
+ }
+
+ fun addPrivateShortcutToHomescreen() {
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Waiting for $waitingTime ms until finding the \"Add private browsing shortcut\" button")
+ mDevice.wait(
+ Until.findObject(text("Add private browsing shortcut")),
+ waitingTime,
+ )
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Waited for $waitingTime ms until the \"Add private browsing shortcut\" button was found")
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Trying to click the \"Add private browsing shortcut\" button")
+ addPrivateBrowsingShortcutButton().click()
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Clicked the \"Add private browsing shortcut\" button")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Waiting for $waitingTime ms until finding the \"Add automatically\" button")
+ mDevice.wait(Until.findObject(By.textContains("add automatically")), waitingTime)
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Waited for $waitingTime ms until the \"Add automatically\" button was found")
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Trying to click the \"Add automatically\" button")
+ addAutomaticallyButton().click()
+ Log.i(TAG, "addPrivateShortcutToHomescreen: Clicked the \"Add automatically\" button")
+ }
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(ViewActions.click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun openPrivateBrowsingShortcut(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "openPrivateBrowsingShortcut: Trying to click the \"Private $appName\" shortcut icon")
+ privateBrowsingShortcutIcon().click()
+ Log.i(TAG, "openPrivateBrowsingShortcut: Clicked the \"Private $appName\" shortcut icon")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+ }
+}
+
+private fun openLinksInPrivateTabSwitch() =
+ onView(withText("Open links in a private tab"))
+
+private fun screenshotsInPrivateModeSwitch() =
+ onView(withText("Allow screenshots in private browsing"))
+
+private fun addPrivateBrowsingShortcutButton() = onView(withText("Add private browsing shortcut"))
+
+private fun goBackButton() = onView(withContentDescription("Navigate up"))
+
+private fun addAutomaticallyButton() =
+ mDevice.findObject(UiSelector().textStartsWith("add automatically"))
+
+private fun cancelShortcutAdditionButton() =
+ mDevice.findObject(UiSelector().textContains("CANCEL"))
+
+private fun privateBrowsingShortcutIcon() = mDevice.findObject(text("Private $appName"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt
new file mode 100644
index 0000000000..c5246f8609
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt
@@ -0,0 +1,633 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnySibling
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.closeSoftKeyboard
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.action.ViewActions.clearText
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.typeText
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withChild
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.endsWith
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getAvailableSearchEngines
+import org.mozilla.fenix.helpers.DataGenerationHelper.getRegionSearchEnginesList
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.hasCousin
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.isChecked
+import org.mozilla.fenix.helpers.isEnabled
+
+/**
+ * Implementation of Robot Pattern for the settings search sub menu.
+ */
+class SettingsSubMenuSearchRobot {
+ fun verifyToolbarText(title: String) {
+ Log.i(TAG, "verifyToolbarText: Trying to verify that the $title toolbar title is visible")
+ onView(
+ allOf(
+ withId(R.id.navigationToolbar),
+ hasDescendant(withContentDescription(R.string.action_bar_up_description)),
+ hasDescendant(withText(title)),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyToolbarText: Verified that the $title toolbar title is visible")
+ }
+
+ fun verifySearchEnginesSectionHeader() {
+ Log.i(TAG, "verifySearchEnginesSectionHeader: Trying to verify that the \"Search engines\" heading is displayed")
+ onView(withText("Search engines")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifySearchEnginesSectionHeader: Verified that the \"Search engines\" heading is displayed")
+ }
+
+ fun verifyDefaultSearchEngineHeader() {
+ Log.i(TAG, "verifyDefaultSearchEngineHeader: Trying to verify that the \"Default search engine\" option is displayed")
+ defaultSearchEngineHeader
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyDefaultSearchEngineHeader: Verified that the \"Default search engine\" option is displayed")
+ }
+
+ fun verifyDefaultSearchEngineSummary(engineName: String) {
+ Log.i(TAG, "verifyDefaultSearchEngineSummary: Trying to verify that the \"Default search engine\" option has $engineName as summary")
+ defaultSearchEngineHeader.check(matches(hasSibling(withText(engineName))))
+ Log.i(TAG, "verifyDefaultSearchEngineSummary: Verified that the \"Default search engine\" option has $engineName as summary")
+ }
+
+ fun verifyManageSearchShortcutsHeader() {
+ Log.i(TAG, "verifyManageSearchShortcutsHeader: Trying to verify that the \"Manage alternative search engines\" option is displayed")
+ manageSearchShortcutsHeader.check(matches(isDisplayed()))
+ Log.i(TAG, "verifyManageSearchShortcutsHeader: Verified that the \"Manage alternative search engines\" option is displayed")
+ }
+
+ fun verifyManageShortcutsSummary() {
+ Log.i(TAG, "verifyManageShortcutsSummary: Trying to verify that the \"Manage alternative search engines\" option has \"Edit engines visible in the search menu\" as summary")
+ manageSearchShortcutsHeader
+ .check(matches(hasSibling(withText("Edit engines visible in the search menu"))))
+ Log.i(TAG, "verifyManageShortcutsSummary: Verified that the \"Manage alternative search engines\" option has \"Edit engines visible in the search menu\" as summary")
+ }
+
+ fun verifyEnginesShortcutsListHeader() =
+ assertUIObjectExists(itemWithText("Engines visible on the search menu"))
+
+ fun verifyAddressBarSectionHeader() {
+ Log.i(TAG, "verifyAddressBarSectionHeader: Trying to verify that the \"Address bar - Firefox Suggest\" heading is displayed")
+ onView(withText("Address bar - Firefox Suggest")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyAddressBarSectionHeader: Verified that the \"Address bar - Firefox Suggest\" heading is displayed")
+ }
+
+ fun verifyDefaultSearchEngineList() {
+ Log.i(TAG, "verifyDefaultSearchEngineList: Trying to verify that the \"Google\" search engine option has a favicon")
+ defaultSearchEngineOption("Google").check(matches(hasSibling(withId(R.id.engine_icon))))
+ Log.i(TAG, "verifyDefaultSearchEngineList: Verified that the \"Google\" search engine option has a favicon")
+ Log.i(TAG, "verifyDefaultSearchEngineList: Trying to verify that the \"Google\" search engine option is displayed")
+ defaultSearchEngineOption("Google").check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultSearchEngineList: Verified that the \"Google\" search engine option is displayed")
+ Log.i(TAG, "verifyDefaultSearchEngineList: Trying to verify that the \"Bing\" search engine option has a favicon")
+ defaultSearchEngineOption("Bing").check(matches(hasSibling(withId(R.id.engine_icon))))
+ Log.i(TAG, "verifyDefaultSearchEngineList: Verified that the \"Bing\" search engine option has a favicon")
+ Log.i(TAG, "verifyDefaultSearchEngineList: Trying to verify that the \"Bing\" search engine option is displayed")
+ defaultSearchEngineOption("Bing").check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultSearchEngineList: Verified that the \"Bing\" search engine option is displayed")
+ Log.i(TAG, "verifyDefaultSearchEngineList: Trying to verify that the \"DuckDuckGo\" search engine option has a favicon")
+ defaultSearchEngineOption("DuckDuckGo").check(matches(hasSibling(withId(R.id.engine_icon))))
+ Log.i(TAG, "verifyDefaultSearchEngineList: Verified that the \"DuckDuckGo\" search engine option has a favicon")
+ Log.i(TAG, "verifyDefaultSearchEngineList: Trying to verify that the \"DuckDuckGo\" search engine option is displayed")
+ defaultSearchEngineOption("DuckDuckGo").check(matches(isDisplayed()))
+ Log.i(TAG, "verifyDefaultSearchEngineList: Verified that the \"DuckDuckGo\" search engine option is displayed")
+ assertUIObjectExists(addSearchEngineButton())
+ }
+
+ fun verifyManageShortcutsList(testRule: ComposeTestRule) {
+ val availableShortcutsEngines = getRegionSearchEnginesList() + getAvailableSearchEngines()
+
+ availableShortcutsEngines.forEach {
+ Log.i(TAG, "verifyManageShortcutsList: Trying to verify that the ${it.name} alternative search engine is displayed")
+ testRule.onNodeWithText(it.name)
+ .assert(hasAnySibling(hasContentDescription("${it.name} search engine")))
+ .assertIsDisplayed()
+ Log.i(TAG, "verifyManageShortcutsList: Verify that the ${it.name} alternative search engine is displayed")
+ }
+
+ assertUIObjectExists(addSearchEngineButton())
+ }
+
+ /**
+ * Method that verifies the selected engines inside the Manage search shortcuts list.
+ */
+ fun verifySearchShortcutChecked(vararg engineShortcut: EngineShortcut) {
+ engineShortcut.forEach {
+ val shortcutIsChecked = mDevice.findObject(UiSelector().text(it.name))
+ .getFromParent(
+ UiSelector().index(it.checkboxIndex),
+ ).isChecked
+
+ if (it.isChecked) {
+ Log.i(TAG, "verifySearchShortcutChecked: Trying to verify that ${it.name}'s alternative search engine check box is checked")
+ assertTrue("$TAG: ${it.name} alternative search engine check box is not checked", shortcutIsChecked)
+ Log.i(TAG, "verifySearchShortcutChecked: Verified that the ${it.name}'s alternative search engine check box is checked")
+ } else {
+ Log.i(TAG, "verifySearchShortcutChecked: Trying to verify that the ${it.name}'s alternative search engine check box is not checked")
+ assertFalse("$TAG: ${it.name} alternative search engine check box is checked", shortcutIsChecked)
+ Log.i(TAG, "verifySearchShortcutChecked: Verified that the ${it.name}'s alternative search engine check box is not checked")
+ }
+ }
+ }
+
+ fun verifyAutocompleteURlsIsEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifyAutocompleteURlsIsEnabled: Trying to verify that the \"Autocomplete URLs\" toggle is checked: $enabled")
+ autocompleteSwitchButton()
+ .check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
+ Log.i(TAG, "verifyAutocompleteURlsIsEnabled: Verified that the \"Autocomplete URLs\" toggle is checked: $enabled")
+ }
+
+ fun verifyShowSearchSuggestionsEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifyShowSearchSuggestionsEnabled: Trying to verify that the \"Show search suggestions\" toggle is checked: $enabled")
+ showSearchSuggestionSwitchButton()
+ .check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
+ Log.i(TAG, "verifyShowSearchSuggestionsEnabled: Verified that the \"Show search suggestions\" toggle is checked: $enabled")
+ }
+
+ fun verifyShowSearchSuggestionsInPrivateEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifyShowSearchSuggestionsInPrivateEnabled: Trying to verify that the \"Show in private sessions\" check box is checked: $enabled")
+ showSuggestionsInPrivateModeSwitch()
+ .check(
+ matches(
+ hasSibling(
+ withChild(
+ allOf(
+ withClassName(endsWith("CheckBox")),
+ isChecked(enabled),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyShowSearchSuggestionsInPrivateEnabled: Verified that the \"Show in private sessions\" check box is checked: $enabled")
+ }
+
+ fun verifyShowClipboardSuggestionsEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifyShowClipboardSuggestionsEnabled: Trying to verify that the \"Show clipboard suggestions\" option is visible")
+ showClipboardSuggestionSwitch().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyShowClipboardSuggestionsEnabled: Verified that the \"Show clipboard suggestions\" option is visible")
+ Log.i(TAG, "verifyShowClipboardSuggestionsEnabled: Trying to verify that the \"Show clipboard suggestions\" toggle is checked: $enabled")
+ showClipboardSuggestionSwitch().check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
+ Log.i(TAG, "verifyShowClipboardSuggestionsEnabled: Verified that the \"Show clipboard suggestions\" toggle is checked: $enabled")
+ }
+
+ fun verifySearchBrowsingHistoryEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifySearchBrowsingHistoryEnabled: Trying to verify that the \"Search browsing history\" option is visible")
+ searchHistorySwitchButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifySearchBrowsingHistoryEnabled: Verified that the \"Search browsing history\" option is visible")
+ Log.i(TAG, "verifySearchBrowsingHistoryEnabled: Trying to verify that the \"Search browsing history\" toggle is checked: $enabled")
+ searchHistorySwitchButton().check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
+ Log.i(TAG, "verifySearchBrowsingHistoryEnabled: Verified that the \"Search browsing history\" toggle is checked: $enabled")
+ }
+
+ fun verifySearchBookmarksEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifySearchBookmarksEnabled: Trying to verify that the \"Search bookmarks\" option is visible")
+ searchBookmarksSwitchButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifySearchBookmarksEnabled: Verified that the \"Search bookmarks\" option is visible")
+ Log.i(TAG, "verifySearchBookmarksEnabled: Trying to verify that the \"Search bookmarks\" toggle is checked: $enabled")
+ searchBookmarksSwitchButton().check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
+ Log.i(TAG, "verifySearchBookmarksEnabled: Verified that the \"Search bookmarks\" toggle is checked: $enabled")
+ }
+
+ fun verifySearchSyncedTabsEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifySearchSyncedTabsEnabled: Trying to verify that the \"Search synced tabs\" option is visible")
+ searchSyncedTabsSwitchButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifySearchSyncedTabsEnabled: Verified that the \"Search synced tabs\" option is visible")
+ Log.i(TAG, "verifySearchSyncedTabsEnabled: Trying to verify that the \"Search synced tabs\" toggle is checked: $enabled")
+ searchSyncedTabsSwitchButton().check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
+ Log.i(TAG, "verifySearchSyncedTabsEnabled: Verified that the \"Search synced tabs\" toggle is checked: $enabled")
+ }
+
+ fun verifyVoiceSearchEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifyVoiceSearchEnabled: Trying to verify that the \"Show voice search\" option is visible")
+ voiceSearchSwitchButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyVoiceSearchEnabled: Verified that the \"Show voice search\" option is visible")
+ Log.i(TAG, "verifyVoiceSearchEnabled: Trying to verify that the \"Show voice search\" toggle is checked: $enabled")
+ voiceSearchSwitchButton().check(matches(hasCousin(allOf(withClassName(endsWith("Switch")), isChecked(enabled)))))
+ Log.i(TAG, "verifyVoiceSearchEnabled: Verified that the \"Show voice search\" toggle is checked: $enabled")
+ }
+
+ fun openDefaultSearchEngineMenu() {
+ Log.i(TAG, "openDefaultSearchEngineMenu: Trying to click the \"Default search engine\" button")
+ defaultSearchEngineHeader.click()
+ Log.i(TAG, "openDefaultSearchEngineMenu: Clicked the \"Default search engine\" button")
+ }
+
+ fun openManageShortcutsMenu() {
+ Log.i(TAG, "openManageShortcutsMenu: Trying to click the \"Manage alternative search engines\" button")
+ manageSearchShortcutsHeader.click()
+ Log.i(TAG, "openManageShortcutsMenu: Clicked the \"Manage alternative search engines\" button")
+ }
+
+ fun changeDefaultSearchEngine(searchEngineName: String) {
+ Log.i(TAG, "changeDefaultSearchEngine: Trying to verify that the $searchEngineName option is visible")
+ onView(withText(searchEngineName)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "changeDefaultSearchEngine: Verified that the $searchEngineName option is visible")
+ Log.i(TAG, "changeDefaultSearchEngine: Trying to click the $searchEngineName option")
+ onView(withText(searchEngineName)).perform(click())
+ Log.i(TAG, "changeDefaultSearchEngine: Clicked the $searchEngineName option")
+ }
+
+ fun selectSearchShortcut(shortcut: EngineShortcut) {
+ Log.i(TAG, "selectSearchShortcut: Trying to click ${shortcut.name}'s alternative search engine check box")
+ mDevice.findObject(UiSelector().text(shortcut.name))
+ .getFromParent(UiSelector().index(shortcut.checkboxIndex))
+ .click()
+ Log.i(TAG, "selectSearchShortcut: Clicked ${shortcut.name}'s alternative search engine check box")
+ }
+
+ fun toggleAutocomplete() {
+ Log.i(TAG, "toggleAutocomplete: Trying to click the \"Autocomplete URLs\" toggle")
+ autocompleteSwitchButton().click()
+ Log.i(TAG, "toggleAutocomplete: Clicked the \"Autocomplete URLs\" toggle")
+ }
+
+ fun toggleShowSearchSuggestions() {
+ Log.i(TAG, "toggleShowSearchSuggestions: Trying to click the \"Show search suggestions\" toggle")
+ showSearchSuggestionSwitchButton().click()
+ Log.i(TAG, "toggleShowSearchSuggestions: Clicked the \"Show search suggestions\" toggle")
+ }
+
+ fun toggleVoiceSearch() {
+ Log.i(TAG, "toggleVoiceSearch: Trying to click the \"Show voice search\" toggle")
+ voiceSearchSwitchButton().perform(click())
+ Log.i(TAG, "toggleVoiceSearch: Clicked the \"Show voice search\" toggle")
+ }
+
+ fun toggleClipboardSuggestion() {
+ Log.i(TAG, "toggleClipboardSuggestion: Trying to click the \"Show clipboard suggestions\" toggle")
+ showClipboardSuggestionSwitch().click()
+ Log.i(TAG, "toggleClipboardSuggestion: Clicked the \"Show clipboard suggestions\" toggle")
+ }
+
+ fun switchSearchHistoryToggle() {
+ Log.i(TAG, "switchSearchHistoryToggle: Trying to click the \"Search browsing history\" toggle")
+ searchHistorySwitchButton().click()
+ Log.i(TAG, "switchSearchHistoryToggle: Clicked the \"Search browsing history\" toggle")
+ }
+
+ fun switchSearchBookmarksToggle() {
+ Log.i(TAG, "switchSearchBookmarksToggle: Trying to click the \"Search bookmarks\" toggle")
+ searchBookmarksSwitchButton().click()
+ Log.i(TAG, "switchSearchBookmarksToggle: Clicked the \"Search bookmarks\" toggle")
+ }
+
+ fun switchShowSuggestionsInPrivateSessionsToggle() {
+ Log.i(TAG, "switchShowSuggestionsInPrivateSessionsToggle: Trying to click the \"Show in private sessions\" check box")
+ showSuggestionsInPrivateModeSwitch().click()
+ Log.i(TAG, "switchShowSuggestionsInPrivateSessionsToggle: Clicked the \"Show in private sessions\" check box")
+ }
+
+ fun openAddSearchEngineMenu() {
+ Log.i(TAG, "openAddSearchEngineMenu: Trying to click the \"Add search engine\" button")
+ addSearchEngineButton().click()
+ Log.i(TAG, "openAddSearchEngineMenu: Clicked the \"Add search engine\" button")
+ }
+
+ fun verifyEngineListContains(searchEngineName: String, shouldExist: Boolean) =
+ assertUIObjectExists(itemWithText(searchEngineName), exists = shouldExist)
+
+ fun verifyDefaultSearchEngineSelected(searchEngineName: String) {
+ Log.i(TAG, "verifyDefaultSearchEngineSelected: Trying to verify that $searchEngineName's radio button is checked")
+ defaultSearchEngineOption(searchEngineName).check(matches(isChecked(true)))
+ Log.i(TAG, "verifyDefaultSearchEngineSelected: Verified that $searchEngineName's radio button is checked")
+ }
+
+ fun verifySaveSearchEngineButtonEnabled(enabled: Boolean) {
+ Log.i(TAG, "verifySaveSearchEngineButtonEnabled: Trying to verify that the \"Save\" button is enabled")
+ addSearchEngineSaveButton().check(matches(isEnabled(enabled)))
+ Log.i(TAG, "verifySaveSearchEngineButtonEnabled: Verified that the \"Save\" button is enabled")
+ }
+
+ fun saveNewSearchEngine() {
+ Log.i(TAG, "saveNewSearchEngine: Trying to perform \"Close soft keyboard\" action")
+ closeSoftKeyboard()
+ Log.i(TAG, "saveNewSearchEngine: Performed \"Close soft keyboard\" action")
+ Log.i(TAG, "saveNewSearchEngine: Trying to click the \"Save\" button")
+ addSearchEngineSaveButton().click()
+ Log.i(TAG, "saveNewSearchEngine: Clicked the \"Save\" button")
+ }
+
+ fun typeCustomEngineDetails(engineName: String, engineURL: String) {
+ try {
+ Log.i(TAG, "typeCustomEngineDetails: Trying to clear the \"Search engine name\" text field")
+ mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
+ Log.i(TAG, "typeCustomEngineDetails: Cleared the \"Search engine name\" text field")
+ Log.i(TAG, "typeCustomEngineDetails: Trying to set the \"Search engine name\" text field to: $engineName")
+ mDevice.findObject(By.res("$packageName:id/edit_engine_name")).text = engineName
+ Log.i(TAG, "typeCustomEngineDetails: The \"Search engine name\" text field text was set to: $engineName")
+ assertUIObjectExists(
+ itemWithResIdAndText("$packageName:id/edit_engine_name", engineName),
+ )
+ Log.i(TAG, "typeCustomEngineDetails: Trying to clear the \"URL to use for search\" text field")
+ mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
+ Log.i(TAG, "typeCustomEngineDetails: Cleared the \"URL to use for search\" text field")
+ Log.i(TAG, "typeCustomEngineDetails: Trying to set the \"URL to use for search\" text field to: $engineURL")
+ mDevice.findObject(By.res("$packageName:id/edit_search_string")).text = engineURL
+ Log.i(TAG, "typeCustomEngineDetails: The \"URL to use for search\" text field text was set to: $engineURL")
+ assertUIObjectExists(
+ itemWithResIdAndText("$packageName:id/edit_search_string", engineURL),
+ )
+ } catch (e: AssertionError) {
+ Log.i(TAG, "typeCustomEngineDetails: AssertionError caught, executing fallback methods")
+ Log.i(TAG, "typeCustomEngineDetails: Trying to clear the \"Search engine name\" text field")
+ mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
+ Log.i(TAG, "typeCustomEngineDetails: Cleared the \"Search engine name\" text field")
+ Log.i(TAG, "typeCustomEngineDetails: Trying to set the \"Search engine name\" text field to: $engineName")
+ mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName)
+ Log.i(TAG, "typeCustomEngineDetails: The \"Search engine name\" text field text was set to: $engineName")
+ assertUIObjectExists(
+ itemWithResIdAndText("$packageName:id/edit_engine_name", engineName),
+ )
+ Log.i(TAG, "typeCustomEngineDetails: Trying to clear the \"URL to use for search\" text field")
+ mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
+ Log.i(TAG, "typeCustomEngineDetails: Cleared the \"URL to use for search\" text field")
+ Log.i(TAG, "typeCustomEngineDetails: Trying to set the \"URL to use for search\" text field to: $engineURL")
+ mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
+ Log.i(TAG, "typeCustomEngineDetails: The \"URL to use for search\" text field text was set to: $engineURL")
+ assertUIObjectExists(
+ itemWithResIdAndText("$packageName:id/edit_search_string", engineURL),
+ )
+ }
+ }
+
+ fun typeSearchEngineSuggestionString(searchSuggestionString: String) {
+ Log.i(TAG, "typeSearchEngineSuggestionString: Trying to click the \"Search suggestion API URL\" text field")
+ onView(withId(R.id.edit_suggest_string)).click()
+ Log.i(TAG, "typeSearchEngineSuggestionString: Clicked the \"Search suggestion API URL\" text field")
+ Log.i(TAG, "typeSearchEngineSuggestionString: Trying to clear the \"Search suggestion API URL\" text field")
+ onView(withId(R.id.edit_suggest_string)).perform(clearText())
+ Log.i(TAG, "typeSearchEngineSuggestionString: Cleared the \"Search suggestion API URL\" text field")
+ Log.i(TAG, "typeSearchEngineSuggestionString: Trying to type $searchSuggestionString in the \"Search suggestion API URL\" text field")
+ onView(withId(R.id.edit_suggest_string)).perform(typeText(searchSuggestionString))
+ Log.i(TAG, "typeSearchEngineSuggestionString: Typed $searchSuggestionString in the \"Search suggestion API URL\" text field")
+ }
+
+ // Used in the non-Compose Default search engines menu
+ fun openEngineOverflowMenu(searchEngineName: String) {
+ Log.i(TAG, "openEngineOverflowMenu: Waiting for $waitingTimeShort ms for $searchEngineName's three dot button to exist")
+ threeDotMenu(searchEngineName).waitForExists(waitingTimeShort)
+ Log.i(TAG, "openEngineOverflowMenu: Waited for $waitingTimeShort ms for $searchEngineName's three dot button to exist")
+ Log.i(TAG, "openEngineOverflowMenu: Trying to click $searchEngineName's three dot button")
+ threeDotMenu(searchEngineName).click()
+ Log.i(TAG, "openEngineOverflowMenu: Clicked $searchEngineName's three dot button")
+ }
+
+ // Used in the composable Manage shortcuts menu, otherwise the overflow menu is not visible
+ fun openCustomShortcutOverflowMenu(testRule: ComposeTestRule, searchEngineName: String) {
+ Log.i(TAG, "openCustomShortcutOverflowMenu: Trying to click $searchEngineName's three dot button")
+ testRule.onNode(overflowMenuWithSiblingText(searchEngineName)).performClick()
+ Log.i(TAG, "openCustomShortcutOverflowMenu: Clicked $searchEngineName's three dot button")
+ }
+
+ fun clickEdit() {
+ Log.i(TAG, "clickEdit: Trying to click the \"Edit\" button")
+ onView(withText("Edit")).click()
+ Log.i(TAG, "clickEdit: Clicked the \"Edit\" button")
+ }
+
+ // Used in the Default search engine menu
+ fun clickDeleteSearchEngine() {
+ Log.i(TAG, "clickDeleteSearchEngine: Trying to click the \"Delete\" button")
+ mDevice.findObject(
+ UiSelector().textContains(getStringResource(R.string.search_engine_delete)),
+ ).click()
+ Log.i(TAG, "clickDeleteSearchEngine: Clicked the \"Delete\" button")
+ }
+
+ // Used in the composable Manage search shortcuts menu, otherwise the overflow menu is not visible
+ fun clickDeleteSearchEngine(testRule: ComposeTestRule) {
+ Log.i(TAG, "clickDeleteSearchEngine: Trying to click the \"Delete\" button")
+ testRule.onNodeWithText("Delete").performClick()
+ Log.i(TAG, "clickDeleteSearchEngine: Clicked the \"Delete\" button")
+ }
+
+ fun saveEditSearchEngine() {
+ Log.i(TAG, "saveEditSearchEngine: Trying to click the \"Save\" button")
+ onView(withId(R.id.save_button)).click()
+ Log.i(TAG, "saveEditSearchEngine: Clicked the \"Save\" button")
+ assertUIObjectExists(itemContainingText("Saved"))
+ }
+
+ fun verifyInvalidTemplateSearchStringFormatError() {
+ Log.i(TAG, "verifyInvalidTemplateSearchStringFormatError: Trying to perform \"Close soft keyboard\" action")
+ closeSoftKeyboard()
+ Log.i(TAG, "verifyInvalidTemplateSearchStringFormatError: Performed \"Close soft keyboard\" action")
+ Log.i(TAG, "verifyInvalidTemplateSearchStringFormatError: Trying to verify that the \"Check that search string matches Example format\" error message is displayed")
+ onView(withText(getStringResource(R.string.search_add_custom_engine_error_missing_template)))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyInvalidTemplateSearchStringFormatError: Verified that the \"Check that search string matches Example format\" error message is displayed")
+ }
+
+ fun verifyErrorConnectingToSearchString(searchEngineName: String) {
+ Log.i(TAG, "verifyErrorConnectingToSearchString: Trying to perform \"Close soft keyboard\" action")
+ closeSoftKeyboard()
+ Log.i(TAG, "verifyErrorConnectingToSearchString: Performed \"Close soft keyboard\" action")
+ Log.i(TAG, "verifyErrorConnectingToSearchString: Trying to verify that the \"Error connecting to $searchEngineName\" error message is displayed")
+ onView(withText(getStringResource(R.string.search_add_custom_engine_error_cannot_reach, searchEngineName)))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyErrorConnectingToSearchString: Verified that the \"Error connecting to $searchEngineName\" error message is displayed")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "goBack: Waited for device to be idle")
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun clickCustomSearchStringLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickCustomSearchStringLearnMoreLink: Trying to click the \"Search string URL\" learn more link")
+ onView(withId(R.id.custom_search_engines_learn_more)).click()
+ Log.i(TAG, "clickCustomSearchStringLearnMoreLink: Clicked the \"Search string URL\" learn more link")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickCustomSearchSuggestionsLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickCustomSearchSuggestionsLearnMoreLink: Trying to click the \"Search suggestions API\" learn more link")
+ onView(withId(R.id.custom_search_suggestions_learn_more)).click()
+ Log.i(TAG, "clickCustomSearchSuggestionsLearnMoreLink: Clicked the \"Search suggestions API\" learn more link")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+/**
+ * Matches search shortcut items inside the 'Manage search shortcuts' menu
+ * @param name, of type String, should be the name of the search engine.
+ * @param checkboxIndex, of type Int, is the checkbox' index afferent to the search engine.
+ * @param isChecked, of type Boolean, should show if the checkbox is expected to be checked.
+ */
+class EngineShortcut(
+ val name: String,
+ val checkboxIndex: Int,
+ val isChecked: Boolean = true,
+)
+
+private val defaultSearchEngineHeader = onView(withText("Default search engine"))
+
+private val manageSearchShortcutsHeader = onView(withText("Manage alternative search engines"))
+
+private fun searchHistorySwitchButton(): ViewInteraction {
+ Log.i(TAG, "searchHistorySwitchButton: Trying to perform scroll action to the \"Search browsing history\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Search browsing history")),
+ ),
+ )
+ Log.i(TAG, "searchHistorySwitchButton: Performed scroll action to the \"Search browsing history\" option")
+ return onView(withText("Search browsing history"))
+}
+
+private fun searchBookmarksSwitchButton(): ViewInteraction {
+ Log.i(TAG, "searchBookmarksSwitchButton: Trying to perform scroll action to the \"Search bookmarks\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Search bookmarks")),
+ ),
+ )
+ Log.i(TAG, "searchBookmarksSwitchButton: Performed scroll action to the \"Search bookmarks\" option")
+ return onView(withText("Search bookmarks"))
+}
+
+private fun searchSyncedTabsSwitchButton(): ViewInteraction {
+ Log.i(TAG, "searchSyncedTabsSwitchButton: Trying to perform scroll action to the \"Search synced tabs\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Search synced tabs")),
+ ),
+ )
+ Log.i(TAG, "searchSyncedTabsSwitchButton: Performed scroll action to the \"Search synced tabs\" option")
+ return onView(withText("Search synced tabs"))
+}
+
+private fun voiceSearchSwitchButton(): ViewInteraction {
+ Log.i(TAG, "voiceSearchSwitchButton: Trying to perform scroll action to the \"Show voice search\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Show voice search")),
+ ),
+ )
+ Log.i(TAG, "voiceSearchSwitchButton: Performed scroll action to the \"Show voice search\" option")
+ return onView(withText("Show voice search"))
+}
+
+private fun autocompleteSwitchButton(): ViewInteraction {
+ Log.i(TAG, "autocompleteSwitchButton: Trying to perform scroll action to the \"Autocomplete URLs\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(getStringResource(R.string.preferences_enable_autocomplete_urls))),
+ ),
+ )
+ Log.i(TAG, "autocompleteSwitchButton: Performed scroll action to the \"Autocomplete URLs\" option")
+ return onView(withText(getStringResource(R.string.preferences_enable_autocomplete_urls)))
+}
+
+private fun showSearchSuggestionSwitchButton(): ViewInteraction {
+ Log.i(TAG, "showSearchSuggestionSwitchButton: Trying to perform scroll action to the \"Show search suggestions\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Show search suggestions")),
+ ),
+ )
+ Log.i(TAG, "showSearchSuggestionSwitchButton: Performed scroll action to the \"Show search suggestions\" option")
+ return onView(withText("Show search suggestions"))
+}
+
+private fun showClipboardSuggestionSwitch(): ViewInteraction {
+ Log.i(TAG, "showClipboardSuggestionSwitch: Trying to perform scroll action to the \"Show clipboard suggestions\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(getStringResource(R.string.preferences_show_clipboard_suggestions))),
+ ),
+ )
+ Log.i(TAG, "showClipboardSuggestionSwitch: Performed scroll action to the \"Show clipboard suggestions\" option")
+ return onView(withText(getStringResource(R.string.preferences_show_clipboard_suggestions)))
+}
+
+private fun showSuggestionsInPrivateModeSwitch(): ViewInteraction {
+ Log.i(TAG, "showSuggestionsInPrivateModeSwitch: Trying to perform scroll action to the \"Show in private sessions\" option")
+ onView(withId(androidx.preference.R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private))),
+ ),
+ )
+ Log.i(TAG, "showSuggestionsInPrivateModeSwitch: Performed scroll action to the \"Show in private sessions\" option")
+ return onView(withText(getStringResource(R.string.preferences_show_search_suggestions_in_private)))
+}
+
+private fun goBackButton() =
+ onView(CoreMatchers.allOf(withContentDescription("Navigate up")))
+
+private fun addSearchEngineButton() = mDevice.findObject(UiSelector().text("Add search engine"))
+
+private fun addSearchEngineSaveButton() = onView(withId(R.id.save_button))
+
+private fun threeDotMenu(searchEngineName: String) =
+ mDevice.findObject(UiSelector().text(searchEngineName))
+ .getFromParent(UiSelector().description("More options"))
+
+private fun defaultSearchEngineOption(searchEngineName: String) =
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(searchEngineName)),
+ ),
+ )
+
+private fun overflowMenuWithSiblingText(text: String): SemanticsMatcher =
+ hasAnySibling(hasText(text)) and hasContentDescription("More options")
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSetDefaultBrowserRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSetDefaultBrowserRobot.kt
new file mode 100644
index 0000000000..3b26702b30
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSetDefaultBrowserRobot.kt
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+
+class SettingsSubMenuSetDefaultBrowserRobot {
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "goBack: Waited for device to be idle")
+
+ // We are now in system settings / showing a default browser dialog.
+ // Really want to go back to the app. Not interested in up navigation like in other robots.
+ Log.i(TAG, "clearNotifications: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "clearNotifications: Clicked the device back button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt
new file mode 100644
index 0000000000..3e75776fca
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt
@@ -0,0 +1,316 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithClassName
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.assertIsChecked
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings Site Permissions sub menu.
+ */
+class SettingsSubMenuSitePermissionsCommonRobot {
+
+ fun verifyBlockAudioAndVideoOnMobileDataOnly() {
+ Log.i(TAG, "verifyBlockAudioAndVideoOnMobileDataOnly: Trying to verify that the \"Block audio and video on cellular data only\" option is visible")
+ blockRadioButton().check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
+ Log.i(TAG, "verifyBlockAudioAndVideoOnMobileDataOnly: Verified that the \"Block audio and video on cellular data only\" option is visible")
+ }
+
+ fun verifyBlockAudioOnly() {
+ Log.i(TAG, "verifyBlockAudioOnly: Trying to verify that the \"Block audio only\" option is visible")
+ thirdRadioButton().check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
+ Log.i(TAG, "verifyBlockAudioOnly: Verified that the \"Block audio only\" option is visible")
+ }
+
+ fun verifyVideoAndAudioBlockedRecommended() {
+ Log.i(TAG, "verifyVideoAndAudioBlockedRecommended: Trying to verify that the \"Block audio and video\" option is visible")
+ onView(withId(R.id.fourth_radio)).check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
+ Log.i(TAG, "verifyVideoAndAudioBlockedRecommended: Verified that the \"Block audio and video\" option is visible")
+ }
+
+ fun verifyCheckAutoPlayRadioButtonDefault() {
+ // Allow audio and video
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Trying to verify that the \"Allow audio and video\" radio button is not checked")
+ askToAllowRadioButton()
+ .assertIsChecked(isChecked = false)
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Verified that the \"Allow audio and video\" radio button is not checked")
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Trying to verify that the \"Block audio and video on cellular data only\" radio button is not checked")
+ // Block audio and video on cellular data only
+ blockRadioButton()
+ .assertIsChecked(isChecked = false)
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Verified that the \"Block audio and video on cellular data only\" radio button is not checked")
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Trying to verify that the \"Block audio only\" radio button is checked")
+ // Block audio only (default)
+ thirdRadioButton()
+ .assertIsChecked(isChecked = true)
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Verified that the \"Block audio only\" radio button is checked")
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Trying to verify that the \"Block audio and video\" radio button is not checked")
+ // Block audio and video
+ fourthRadioButton()
+ .assertIsChecked(isChecked = false)
+ Log.i(TAG, "verifyCheckAutoPlayRadioButtonDefault: Verified that the \"Block audio and video\" radio button is not checked")
+ }
+
+ fun verifyAskToAllowButton(isChecked: Boolean = true) {
+ Log.i(TAG, "verifyAskToAllowButton: Trying to verify that the \"Ask to allow\" radio button is checked: $isChecked")
+ onView(withId(R.id.ask_to_allow_radio))
+ .check((matches(isDisplayed()))).assertIsChecked(isChecked)
+ Log.i(TAG, "verifyAskToAllowButton: Verified that the \"Ask to allow\" radio button is checked: $isChecked")
+ }
+
+ fun verifyBlockedButton(isChecked: Boolean = false) {
+ Log.i(TAG, "verifyBlockedButton: Trying to verify that the \"Blocked\" radio button is checked: $isChecked")
+ onView(withId(R.id.block_radio))
+ .check((matches(isDisplayed()))).assertIsChecked(isChecked)
+ Log.i(TAG, "verifyBlockedButton: Verified that the \"Blocked\" radio button is checked: $isChecked")
+ }
+
+ fun verifyBlockedByAndroid() {
+ Log.i(TAG, "verifyBlockedByAndroid: Waiting for $waitingTime ms for the \"Blocked by Android\" heading to exist")
+ blockedByAndroidContainer().waitForExists(waitingTime)
+ Log.i(TAG, "verifyBlockedByAndroid: Waited for $waitingTime ms for the \"Blocked by Android\" heading to exist")
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.phone_feature_blocked_by_android)))
+ }
+
+ fun verifyUnblockedByAndroid() {
+ Log.i(TAG, "verifyUnblockedByAndroid: Waiting for $waitingTime ms for the \"Blocked by Android\" heading to be gone")
+ blockedByAndroidContainer().waitUntilGone(waitingTime)
+ Log.i(TAG, "verifyUnblockedByAndroid: Waited for $waitingTime ms for the \"Blocked by Android\" heading to be gone")
+ assertUIObjectExists(itemContainingText(getStringResource(R.string.phone_feature_blocked_by_android)), exists = false)
+ }
+
+ fun verifyToAllowIt() {
+ Log.i(TAG, "verifyToAllowIt: Trying to verify that the \"To allow it:\" instruction is visible")
+ onView(withText(R.string.phone_feature_blocked_intro)).check(
+ matches(
+ withEffectiveVisibility(
+ Visibility.VISIBLE,
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyToAllowIt: Verified that the \"To allow it:\" instruction is visible")
+ }
+
+ fun verifyGotoAndroidSettings() {
+ Log.i(TAG, "verifyGotoAndroidSettings: Trying to verify that the \"1. Go to Android Settings\" step is visible")
+ onView(withText(R.string.phone_feature_blocked_step_settings)).check(
+ matches(
+ withEffectiveVisibility(Visibility.VISIBLE),
+ ),
+ )
+ Log.i(TAG, "verifyGotoAndroidSettings: Verified that the \"1. Go to Android Settings\" step is visible")
+ }
+
+ fun verifyTapPermissions() {
+ Log.i(TAG, "verifyTapPermissions: Trying to verify that the \"2. Tap Permissions\" step is visible")
+ onView(withText("2. Tap Permissions")).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyTapPermissions: Verified that the \"2. Tap Permissions\" step is visible")
+ }
+
+ fun verifyGoToSettingsButton() {
+ Log.i(TAG, "verifyGoToSettingsButton: Trying to verify that the \"Go to settings\" button is visible")
+ goToSettingsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyGoToSettingsButton: Verified that the \"Go to settings\" button is visible")
+ }
+
+ fun verifySitePermissionsAutoPlaySubMenuItems() {
+ verifyBlockAudioAndVideoOnMobileDataOnly()
+ verifyBlockAudioOnly()
+ verifyVideoAndAudioBlockedRecommended()
+ verifyCheckAutoPlayRadioButtonDefault()
+ }
+
+ fun verifySitePermissionsCommonSubMenuItems() {
+ verifyAskToAllowButton()
+ verifyBlockedButton()
+ }
+
+ fun verifyBlockedByAndroidSection() {
+ verifyBlockedByAndroid()
+ verifyToAllowIt()
+ verifyGotoAndroidSettings()
+ verifyTapPermissions()
+ verifyGoToSettingsButton()
+ }
+
+ fun verifyNotificationSubMenuItems() {
+ verifyNotificationToolbar()
+ verifySitePermissionsCommonSubMenuItems()
+ }
+
+ fun verifySitePermissionsPersistentStorageSubMenuItems() {
+ verifyAskToAllowButton()
+ verifyBlockedButton()
+ }
+
+ fun verifyDRMControlledContentSubMenuItems() {
+ verifyAskToAllowButton()
+ verifyBlockedButton()
+ // Third option is "Allowed"
+ Log.i(TAG, "verifyDRMControlledContentSubMenuItems: Trying to verify that \"Allowed\" is the third radio button")
+ thirdRadioButton().check(matches(withText("Allowed")))
+ Log.i(TAG, "verifyDRMControlledContentSubMenuItems: Verified that \"Allowed\" is the third radio button")
+ }
+
+ fun clickGoToSettingsButton() {
+ Log.i(TAG, "clickGoToSettingsButton: Trying to click the \"Go to settings\" button")
+ goToSettingsButton().click()
+ Log.i(TAG, "clickGoToSettingsButton: Clicked the \"Go to settings\" button")
+ Log.i(TAG, "clickGoToSettingsButton: Waiting for $waitingTime ms for system app info list to exist")
+ mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "clickGoToSettingsButton: Waited for $waitingTime ms for system app info list to exist")
+ }
+
+ fun openAppSystemPermissionsSettings() {
+ Log.i(TAG, "openAppSystemPermissionsSettings: Trying to click the system \"Permissions\" button")
+ mDevice.findObject(UiSelector().textContains("Permissions")).click()
+ Log.i(TAG, "openAppSystemPermissionsSettings: Clicked the system \"Permissions\" button")
+ }
+
+ fun switchAppPermissionSystemSetting(permissionCategory: String, permission: String) {
+ Log.i(TAG, "switchAppPermissionSystemSetting: Trying to click the system permission category: $permissionCategory button")
+ mDevice.findObject(UiSelector().textContains(permissionCategory)).click()
+ Log.i(TAG, "switchAppPermissionSystemSetting: Clicked the system permission category: $permissionCategory button")
+
+ if (permission == "Allow") {
+ Log.i(TAG, "switchAppPermissionSystemSetting: Trying to click the system permission option: $permission")
+ mDevice.findObject(UiSelector().textContains("Allow")).click()
+ Log.i(TAG, "switchAppPermissionSystemSetting: Clicked the system permission option: $permission")
+ } else {
+ Log.i(TAG, "switchAppPermissionSystemSetting: Trying to click the system permission option: $permission")
+ mDevice.findObject(UiSelector().textContains("Deny")).click()
+ Log.i(TAG, "switchAppPermissionSystemSetting: Clicked the system permission option: $permission")
+ }
+ }
+
+ fun goBackToSystemAppPermissionSettings() {
+ Log.i(TAG, "goBackToSystemAppPermissionSettings: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBackToSystemAppPermissionSettings: Clicked the device back button")
+ Log.i(TAG, "goBackToSystemAppPermissionSettings: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "goBackToSystemAppPermissionSettings: Waited for device to be idle for $waitingTime ms")
+ }
+
+ fun goBackToPermissionsSettingsSubMenu() {
+ while (!permissionSettingMenu().waitForExists(waitingTimeShort)) {
+ Log.i(TAG, "goBackToPermissionsSettingsSubMenu: The permissions settings menu does not exist")
+ Log.i(TAG, "goBackToPermissionsSettingsSubMenu: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBackToPermissionsSettingsSubMenu: Clicked the device back button")
+ Log.i(TAG, "goBackToPermissionsSettingsSubMenu: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "goBackToPermissionsSettingsSubMenu: Waited for device to be idle for $waitingTime ms")
+ }
+ }
+
+ fun verifySystemGrantedPermission(permissionCategory: String) {
+ assertUIObjectExists(
+ itemWithClassName("android.widget.RelativeLayout")
+ .getChild(
+ UiSelector()
+ .resourceId("android:id/title")
+ .textContains(permissionCategory),
+ ),
+ itemWithClassName("android.widget.RelativeLayout")
+ .getChild(
+ UiSelector()
+ .resourceId("android:id/summary")
+ .textContains("Only while app is in use"),
+ ),
+ )
+ }
+
+ fun verifyNotificationToolbar() =
+ assertUIObjectExists(
+ itemContainingText(getStringResource(R.string.preference_phone_feature_notification)),
+ itemWithDescription(getStringResource(R.string.action_bar_up_description)),
+ )
+
+ fun selectAutoplayOption(text: String) {
+ Log.i(TAG, "selectAutoplayOption: Trying to click the $text radio button")
+ when (text) {
+ "Allow audio and video" -> askToAllowRadioButton().click()
+ "Block audio and video on cellular data only" -> blockRadioButton().click()
+ "Block audio only" -> thirdRadioButton().click()
+ "Block audio and video" -> fourthRadioButton().click()
+ }
+ Log.i(TAG, "selectAutoplayOption: Clicked the $text radio button button")
+ }
+
+ fun selectPermissionSettingOption(text: String) {
+ Log.i(TAG, "selectPermissionSettingOption: Trying to click the $text radio button")
+ when (text) {
+ "Ask to allow" -> askToAllowRadioButton().click()
+ "Blocked" -> blockRadioButton().click()
+ }
+ Log.i(TAG, "selectPermissionSettingOption: Clicked the $text radio button")
+ }
+
+ fun selectDRMControlledContentPermissionSettingOption(text: String) {
+ Log.i(TAG, "selectDRMControlledContentPermissionSettingOption: Trying to click the $text radio button")
+ when (text) {
+ "Ask to allow" -> askToAllowRadioButton().click()
+ "Blocked" -> blockRadioButton().click()
+ "Allowed" -> thirdRadioButton().click()
+ }
+ Log.i(TAG, "selectDRMControlledContentPermissionSettingOption: Clicked the $text radio button")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsSubMenuSitePermissionsRobot().interact()
+ return SettingsSubMenuSitePermissionsRobot.Transition()
+ }
+ }
+}
+
+// common Blocked radio button for all settings
+private fun blockRadioButton() = onView(withId(R.id.block_radio))
+
+// common Ask to Allow radio button for all settings
+private fun askToAllowRadioButton() = onView(withId(R.id.ask_to_allow_radio))
+
+// common extra 3rd radio button for all settings
+private fun thirdRadioButton() = onView(withId(R.id.third_radio))
+
+// common extra 4th radio button for all settings
+private fun fourthRadioButton() = onView(withId(R.id.fourth_radio))
+
+private fun blockedByAndroidContainer() = mDevice.findObject(UiSelector().resourceId("$packageName:id/permissions_blocked_container"))
+
+private fun permissionSettingMenu() = mDevice.findObject(UiSelector().resourceId("$packageName:id/container"))
+
+private fun goBackButton() =
+ onView(allOf(withContentDescription("Navigate up")))
+
+private fun goToSettingsButton() = onView(withId(R.id.settings_button))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsExceptionsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsExceptionsRobot.kt
new file mode 100644
index 0000000000..8e3f6a9918
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsExceptionsRobot.kt
@@ -0,0 +1,184 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.containsString
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings Site Permissions Notification sub menu.
+ */
+class SettingsSubMenuSitePermissionsExceptionsRobot {
+ fun verifyExceptionsEmptyList() {
+ Log.i(TAG, "verifyExceptionsEmptyList: Waiting for $waitingTime ms for empty exceptions list to exist")
+ mDevice.findObject(UiSelector().text(getStringResource(R.string.no_site_exceptions)))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "verifyExceptionsEmptyList: Waited for $waitingTime ms for empty exceptions list to exist")
+ Log.i(TAG, "verifyExceptionsEmptyList: Trying to verify that the empty exceptions list is displayed")
+ onView(withText(R.string.no_site_exceptions)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyExceptionsEmptyList: Verified that the empty exceptions list is displayed")
+ }
+
+ fun verifyExceptionCreated(url: String, shouldBeDisplayed: Boolean) {
+ if (shouldBeDisplayed) {
+ Log.i(TAG, "verifyExceptionCreated: Waiting for $waitingTime ms for exceptions list to exist")
+ exceptionsList().waitForExists(waitingTime)
+ Log.i(TAG, "verifyExceptionCreated: Waited for $waitingTime ms for exceptions list to exist")
+ Log.i(TAG, "verifyExceptionCreated: Trying to verify that $url is displayed in the exceptions list")
+ onView(withText(containsString(url))).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyExceptionCreated: Verified that $url is displayed in the exceptions list")
+ } else {
+ assertUIObjectIsGone(itemContainingText(url))
+ }
+ }
+
+ fun verifyClearPermissionsDialog() {
+ Log.i(TAG, "verifyClearPermissionsDialog: Trying to verify that the \"Clear permissions\" dialog title is displayed")
+ onView(withText(R.string.clear_permissions)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsDialog: Verified that the \"Clear permissions\" dialog title is displayed")
+ Log.i(TAG, "verifyClearPermissionsDialog: Trying to verify that the \"Are you sure that you want to clear all the permissions on all sites?\" dialog message is displayed")
+ onView(withText(R.string.confirm_clear_permissions_on_all_sites)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsDialog: Verified that the \"Are you sure that you want to clear all the permissions on all sites?\" dialog message is displayed")
+ Log.i(TAG, "verifyClearPermissionsDialog: Trying to verify that the \"Ok\" dialog button is displayed")
+ onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsDialog: Verified that the \"Ok\" dialog button is displayed")
+ Log.i(TAG, "verifyClearPermissionsDialog: Trying to verify that the \"Cancel\" dialog button is displayed")
+ onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsDialog: Verified that the \"Cancel\" dialog button is displayed")
+ }
+
+ // Click button for resetting all of one site's permissions to default
+ fun clickClearPermissionsForOneSite() {
+ swipeToBottom()
+ Log.i(TAG, "clickClearPermissionsForOneSite: Trying to click the \"Clear permissions\" button")
+ onView(withText(R.string.clear_permissions))
+ .check(matches(isDisplayed()))
+ .click()
+ Log.i(TAG, "clickClearPermissionsForOneSite: Clicked the \"Clear permissions\" button")
+ }
+ fun verifyClearPermissionsForOneSiteDialog() {
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Trying to verify that the \"Clear permissions\" dialog title is displayed")
+ onView(withText(R.string.clear_permissions)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Verified that the \"Clear permissions\" dialog title is displayed")
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Trying to verify that the \"Are you sure that you want to clear all the permissions for this site?\" dialog message is displayed")
+ onView(withText(R.string.confirm_clear_permissions_site)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Verified that the \"Are you sure that you want to clear all the permissions for this site?\" dialog message is displayed")
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Trying to verify that the \"Ok\" dialog button is displayed")
+ onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Verified that the \"Ok\" dialog button is displayed")
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Trying to verify that the \"Cancel\" dialog button is displayed")
+ onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearPermissionsForOneSiteDialog: Verified that the \"Cancel\" dialog button is displayed")
+ }
+
+ fun openSiteExceptionsDetails(url: String) {
+ Log.i(TAG, "openSiteExceptionsDetails: Waiting for $waitingTime ms for exceptions list to exist")
+ exceptionsList().waitForExists(waitingTime)
+ Log.i(TAG, "openSiteExceptionsDetails: Waited for $waitingTime ms for exceptions list to exist")
+ Log.i(TAG, "openSiteExceptionsDetails: Trying to click $url exception")
+ onView(withText(containsString(url))).click()
+ Log.i(TAG, "openSiteExceptionsDetails: Clicked $url exception")
+ }
+
+ fun verifyPermissionSettingSummary(setting: String, summary: String) {
+ Log.i(TAG, "verifyPermissionSettingSummary: Trying to verify that $setting permission is $summary and is displayed")
+ onView(
+ allOf(
+ withText(setting),
+ hasSibling(withText(summary)),
+ ),
+ ).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyPermissionSettingSummary: Verified that $setting permission is $summary and is displayed")
+ }
+
+ fun openChangePermissionSettingsMenu(permissionSetting: String) {
+ Log.i(TAG, "openChangePermissionSettingsMenu: Trying to click $permissionSetting permission button")
+ onView(withText(containsString(permissionSetting))).click()
+ Log.i(TAG, "openChangePermissionSettingsMenu: Clicked $permissionSetting permission button")
+ }
+
+ // Click button for resetting all permissions for all websites
+ fun clickClearPermissionsOnAllSites() {
+ Log.i(TAG, "clickClearPermissionsOnAllSites: Waiting for $waitingTime ms for exceptions list to exist")
+ exceptionsList().waitForExists(waitingTime)
+ Log.i(TAG, "clickClearPermissionsOnAllSites: Waited for $waitingTime ms for exceptions list to exist")
+ Log.i(TAG, "clickClearPermissionsOnAllSites: Trying to click the \"Clear permissions on all sites\" button")
+ onView(withId(R.id.delete_all_site_permissions_button))
+ .check(matches(isDisplayed()))
+ .click()
+ Log.i(TAG, "clickClearPermissionsOnAllSites: Clicked the \"Clear permissions on all sites\" button")
+ }
+
+ // Click button for resetting one site permission to default
+ fun clickClearOnePermissionForOneSite() {
+ Log.i(TAG, "clickClearOnePermissionForOneSite: Trying to click the \"Clear permissions\" button")
+ onView(withText(R.string.clear_permission))
+ .check(matches(isDisplayed()))
+ .click()
+ Log.i(TAG, "clickClearOnePermissionForOneSite: Clicked the \"Clear permissions\" button")
+ }
+
+ fun verifyResetPermissionDefaultForThisSiteDialog() {
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Trying to verify that the \"Clear permissions\" dialog title is displayed")
+ onView(withText(R.string.clear_permission)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Verified that the \"Clear permissions\" dialog title is displayed")
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Trying to verify that the \"Are you sure that you want to clear this permission for this site?\" dialog message is displayed")
+ onView(withText(R.string.confirm_clear_permission_site)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Verified that the \"Are you sure that you want to clear this permission for this site?\" dialog message is displayed")
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Trying to verify that the \"Ok\" dialog button is displayed")
+ onView(withText(R.string.clear_permissions_positive)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Verified that the \"Ok\" dialog button is displayed")
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Trying to verify that the \"Cancel\" dialog button is displayed")
+ onView(withText(R.string.clear_permissions_negative)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyResetPermissionDefaultForThisSiteDialog: Verified that the \"Cancel\" dialog button is displayed")
+ }
+
+ fun clickOK() {
+ Log.i(TAG, "clickOK: Trying to click the \"Ok\" dialog button")
+ onView(withText(R.string.clear_permissions_positive)).click()
+ Log.i(TAG, "clickOK: Clicked the \"Ok\" dialog button")
+ }
+
+ fun clickCancel() {
+ Log.i(TAG, "clickCancel: Trying to click the \"Cancel\" dialog button")
+ onView(withText(R.string.clear_permissions_negative)).click()
+ Log.i(TAG, "clickCancel: Clicked the \"Cancel\" dialog button")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsSubMenuSitePermissionsRobot().interact()
+ return SettingsSubMenuSitePermissionsRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(allOf(withContentDescription("Navigate up")))
+
+private fun exceptionsList() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/exceptions"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsRobot.kt
new file mode 100644
index 0000000000..6debbcb4c4
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsRobot.kt
@@ -0,0 +1,235 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.preference.R
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings Site Permissions sub menu.
+ */
+class SettingsSubMenuSitePermissionsRobot {
+
+ fun verifySitePermissionsToolbarTitle() {
+ Log.i(TAG, "verifySitePermissionsToolbarTitle: Trying to verify that the \"Site permissions\" toolbar title is visible")
+ onView(withText("Site permissions")).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySitePermissionsToolbarTitle: Verified that the \"Site permissions\" toolbar title is visible")
+ }
+
+ fun verifyToolbarGoBackButton() {
+ Log.i(TAG, "verifyToolbarGoBackButton: Trying to verify that the navigate up toolbar button is visible")
+ goBackButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyToolbarGoBackButton: Verified that the navigate up toolbar button is visible")
+ }
+
+ fun verifySitePermissionOption(option: String, summary: String = "") {
+ Log.i(TAG, "verifySitePermissionOption: Trying to verify that the $option option with $summary summary is visible")
+ scrollToElementByText(option)
+ onView(
+ allOf(
+ withText(option),
+ hasSibling(withText(summary)),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySitePermissionOption: Trying to verify that the $option option with $summary summary is visible")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click navigate up toolbar button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up toolbar button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun openAutoPlay(
+ interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsCommonRobot.Transition {
+ Log.i(TAG, "openAutoPlay: Trying to perform scroll action to the \"Autoplay\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Autoplay")),
+ ),
+ )
+ Log.i(TAG, "openAutoPlay: Performed scroll action to the \"Autoplay\" button")
+ Log.i(TAG, "openAutoPlay: Trying to click the \"Autoplay\" button")
+ openAutoPlay().click()
+ Log.i(TAG, "openAutoPlay: Clicked the \"Autoplay\" button")
+
+ SettingsSubMenuSitePermissionsCommonRobot().interact()
+ return SettingsSubMenuSitePermissionsCommonRobot.Transition()
+ }
+
+ fun openCamera(
+ interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsCommonRobot.Transition {
+ Log.i(TAG, "openCamera: Trying to perform scroll action to the \"Camera\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Camera")),
+ ),
+ )
+ Log.i(TAG, "openCamera: Performed scroll action to the \"Camera\" button")
+ Log.i(TAG, "openCamera: Trying to click the \"Camera\" button")
+ openCamera().click()
+ Log.i(TAG, "openCamera: Clicked the \"Camera\" button")
+
+ SettingsSubMenuSitePermissionsCommonRobot().interact()
+ return SettingsSubMenuSitePermissionsCommonRobot.Transition()
+ }
+
+ fun openLocation(
+ interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsCommonRobot.Transition {
+ Log.i(TAG, "openLocation: Trying to perform scroll action to the \"Location\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Location")),
+ ),
+ )
+ Log.i(TAG, "openLocation: Performed scroll action to the \"Location\" button")
+ Log.i(TAG, "openLocation: Trying to click the \"Location\" button")
+ openLocation().click()
+ Log.i(TAG, "openLocation: Clicked the \"Location\" button")
+
+ SettingsSubMenuSitePermissionsCommonRobot().interact()
+ return SettingsSubMenuSitePermissionsCommonRobot.Transition()
+ }
+
+ fun openMicrophone(
+ interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsCommonRobot.Transition {
+ Log.i(TAG, "openMicrophone: Trying to perform scroll action to the \"Microphone\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Microphone")),
+ ),
+ )
+ Log.i(TAG, "openMicrophone: Performed scroll action to the \"Microphone\" button")
+ Log.i(TAG, "openMicrophone: Trying to click the \"Microphone\" button")
+ openMicrophone().click()
+ Log.i(TAG, "openMicrophone: Clicked the \"Microphone\" button")
+
+ SettingsSubMenuSitePermissionsCommonRobot().interact()
+ return SettingsSubMenuSitePermissionsCommonRobot.Transition()
+ }
+
+ fun openNotification(
+ interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsCommonRobot.Transition {
+ Log.i(TAG, "openNotification: Trying to perform scroll action to the \"Notification\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Notification")),
+ ),
+ )
+ Log.i(TAG, "openNotification: Performed scroll action to the \"Notification\" button")
+ Log.i(TAG, "openNotification: Trying to click the \"Notification\" button")
+ openNotification().click()
+ Log.i(TAG, "openNotification: Clicked the \"Notification\" button")
+
+ SettingsSubMenuSitePermissionsCommonRobot().interact()
+ return SettingsSubMenuSitePermissionsCommonRobot.Transition()
+ }
+
+ fun openPersistentStorage(
+ interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsCommonRobot.Transition {
+ Log.i(TAG, "openPersistentStorage: Trying to perform scroll action to the \"Persistent Storage\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Persistent Storage")),
+ ),
+ )
+ Log.i(TAG, "openPersistentStorage: Performed scroll action to the \"Persistent Storage\" button")
+ Log.i(TAG, "openPersistentStorage: Trying to click the \"Persistent Storage\" button")
+ openPersistentStorage().click()
+ Log.i(TAG, "openPersistentStorage: Clicked the \"Persistent Storage\" button")
+
+ SettingsSubMenuSitePermissionsCommonRobot().interact()
+ return SettingsSubMenuSitePermissionsCommonRobot.Transition()
+ }
+
+ fun openDRMControlledContent(
+ interact: SettingsSubMenuSitePermissionsCommonRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsCommonRobot.Transition {
+ Log.i(TAG, "openDRMControlledContent: Trying to perform scroll action to the \"DRM-controlled content\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("DRM-controlled content")),
+ ),
+ )
+ Log.i(TAG, "openDRMControlledContent: Performed scroll action to the \"DRM-controlled content\" button")
+ Log.i(TAG, "openDRMControlledContent: Trying to click the \"DRM-controlled content\" button")
+ openDrmControlledContent().click()
+ Log.i(TAG, "openDRMControlledContent: Clicked the \"DRM-controlled content\" button")
+
+ SettingsSubMenuSitePermissionsCommonRobot().interact()
+ return SettingsSubMenuSitePermissionsCommonRobot.Transition()
+ }
+
+ fun openExceptions(
+ interact: SettingsSubMenuSitePermissionsExceptionsRobot.() -> Unit,
+ ): SettingsSubMenuSitePermissionsExceptionsRobot.Transition {
+ Log.i(TAG, "openExceptions: Trying to perform scroll action to the \"Exceptions\" button")
+ onView(withId(R.id.recycler_view)).perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText("Exceptions")),
+ ),
+ )
+ Log.i(TAG, "openExceptions: Performed scroll action to the \"Exceptions\" button")
+ Log.i(TAG, "openExceptions: Trying to click the \"Exceptions\" button")
+ openExceptions().click()
+ Log.i(TAG, "openExceptions: Clicked the \"Exceptions\" button")
+
+ SettingsSubMenuSitePermissionsExceptionsRobot().interact()
+ return SettingsSubMenuSitePermissionsExceptionsRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(withContentDescription("Navigate up"))
+
+private fun openAutoPlay() =
+ onView(allOf(withText("Autoplay")))
+
+private fun openCamera() =
+ onView(allOf(withText("Camera")))
+
+private fun openLocation() =
+ onView(allOf(withText("Location")))
+
+private fun openMicrophone() =
+ onView(allOf(withText("Microphone")))
+
+private fun openNotification() =
+ onView(allOf(withText("Notification")))
+
+private fun openPersistentStorage() =
+ onView(allOf(withText("Persistent Storage")))
+
+private fun openDrmControlledContent() =
+ onView(allOf(withText("DRM-controlled content")))
+
+private fun openExceptions() =
+ onView(allOf(withText("Exceptions")))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuTabsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuTabsRobot.kt
new file mode 100644
index 0000000000..d6e7e557c6
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuTabsRobot.kt
@@ -0,0 +1,140 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.isChecked
+
+/**
+ * Implementation of Robot Pattern for the settings Tabs sub menu.
+ */
+class SettingsSubMenuTabsRobot {
+
+ fun verifyTabViewOptions() {
+ Log.i(TAG, "verifyTabViewOptions: Trying to verify that the \"Tab view\" title is visible")
+ tabViewHeading()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyTabViewOptions: Verified that the \"Tab view\" title is visible")
+ Log.i(TAG, "verifyTabViewOptions: Trying to verify that the \"List\" option is visible")
+ listToggle()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyTabViewOptions: Verified that the \"List\" option is visible")
+ Log.i(TAG, "verifyTabViewOptions: Trying to verify that the \"Grid\" option is visible")
+ gridToggle()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyTabViewOptions: Verified that the \"Grid\" option is visible")
+ }
+
+ fun verifyCloseTabsOptions() {
+ Log.i(TAG, "verifyCloseTabsOptions: Trying to verify that the \"Close tabs\" title is visible")
+ closeTabsHeading()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCloseTabsOptions: Verified that the \"Close tabs\" title is visible")
+ Log.i(TAG, "verifyCloseTabsOptions: Trying to verify that the \"Never\" option is visible")
+ neverOption()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCloseTabsOptions: Verified that the \"Never\" option is visible")
+ Log.i(TAG, "verifyCloseTabsOptions: Trying to verify that the \"After one day\" option is visible")
+ afterOneDayOption()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCloseTabsOptions: Verified that the \"After one day\" option is visible")
+ Log.i(TAG, "verifyCloseTabsOptions: Trying to verify that the \"After one week\" option is visible")
+ afterOneWeekOption()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCloseTabsOptions: Verified that the \"After one week\" option is visible")
+ Log.i(TAG, "verifyCloseTabsOptions: Trying to verify that the \"After one month\" option is visible")
+ afterOneMonthOption()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCloseTabsOptions: Verified that the \"After one month\" option is visible")
+ }
+
+ fun verifyMoveOldTabsToInactiveOptions() {
+ Log.i(TAG, "verifyMoveOldTabsToInactiveOptions: Trying to verify that the \"Move old tabs to inactive\" title is visible")
+ moveOldTabsToInactiveHeading()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyMoveOldTabsToInactiveOptions: Verified that the \"Move old tabs to inactive\" title is visible")
+ Log.i(TAG, "verifyMoveOldTabsToInactiveOptions: Trying to verify that the \"Move old tabs to inactive\" toggle is visible")
+ moveOldTabsToInactiveToggle()
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyMoveOldTabsToInactiveOptions: Verified that the \"Move old tabs to inactive\" toggle is visible")
+ }
+
+ fun verifySelectedCloseTabsOption(closedTabsOption: String) {
+ Log.i(TAG, "verifySelectedCloseTabsOption: Trying to verify that the $closedTabsOption radio button is checked")
+ onView(
+ allOf(
+ withId(R.id.radio_button),
+ hasSibling(withText(closedTabsOption)),
+ ),
+ ).check(matches(isChecked(true)))
+ Log.i(TAG, "verifySelectedCloseTabsOption: Verified that the $closedTabsOption radio button is checked")
+ }
+
+ fun clickClosedTabsOption(closedTabsOption: String) {
+ Log.i(TAG, "clickClosedTabsOption: Trying to click the $closedTabsOption option")
+ when (closedTabsOption) {
+ "Never" -> neverOption().click()
+ "After one day" -> afterOneDayOption().click()
+ "After one week" -> afterOneWeekOption().click()
+ "After one month" -> afterOneMonthOption().click()
+ }
+ Log.i(TAG, "clickClosedTabsOption: Clicked the $closedTabsOption option")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "goBack: Device was idle")
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(ViewActions.click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun tabViewHeading() = onView(withText("Tab view"))
+
+private fun listToggle() = onView(withText("List"))
+
+private fun gridToggle() = onView(withText("Grid"))
+
+private fun closeTabsHeading() = onView(withText("Close tabs"))
+
+private fun manuallyToggle() = onView(withText("Manually"))
+
+private fun neverOption() = onView(withText("Never"))
+
+private fun afterOneDayOption() = onView(withText("After one day"))
+
+private fun afterOneWeekOption() = onView(withText("After one week"))
+
+private fun afterOneMonthOption() = onView(withText("After one month"))
+
+private fun moveOldTabsToInactiveHeading() = onView(withText("Move old tabs to inactive"))
+
+private fun moveOldTabsToInactiveToggle() =
+ onView(withText(R.string.preferences_inactive_tabs_title))
+
+private fun goBackButton() =
+ onView(allOf(ViewMatchers.withContentDescription("Navigate up")))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsTurnOnSyncRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsTurnOnSyncRobot.kt
new file mode 100644
index 0000000000..485a05c665
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsTurnOnSyncRobot.kt
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.hamcrest.CoreMatchers
+import org.hamcrest.Matchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the settings turn on sync option.
+ */
+class SettingsTurnOnSyncRobot {
+ fun verifyUseEmailOption() {
+ Log.i(TAG, "verifyUseEmailOption: Trying to verify that the \"Use email instead\" button is visible")
+ onView(withText("Use email instead"))
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyUseEmailOption: Verified that the \"Use email instead\" button is visible")
+ }
+
+ fun verifyReadyToScanOption() {
+ Log.i(TAG, "verifyReadyToScanOption: Trying to verify that the \"Ready to scan\" button is visible")
+ onView(withText("Ready to scan"))
+ .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyReadyToScanOption: Verified that the \"Ready to scan\" button is visible")
+ }
+
+ fun tapOnUseEmailToSignIn() {
+ Log.i(TAG, "tapOnUseEmailToSignIn: Trying to click the \"Use email instead\" button")
+ useEmailButton().click()
+ Log.i(TAG, "tapOnUseEmailToSignIn: Clicked the \"Use email instead\" button")
+ }
+
+ fun verifyTurnOnSyncToolbarTitle() {
+ Log.i(TAG, "verifyTurnOnSyncToolbarTitle: Trying to verify that the \"Sync and save your data\" toolbar title is displayed")
+ onView(
+ allOf(
+ withParent(withId(R.id.navigationToolbar)),
+ withText(R.string.preferences_sync_2),
+ ),
+ ).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyTurnOnSyncToolbarTitle: Verified that the \"Sync and save your data\" toolbar title is displayed")
+ }
+
+ class Transition {
+ fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().perform(ViewActions.click())
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ SettingsSubMenuLoginsAndPasswordRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
+
+private fun useEmailButton() = onView(withText("Use email instead"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ShareOverlayRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ShareOverlayRobot.kt
new file mode 100644
index 0000000000..13a128131f
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ShareOverlayRobot.kt
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.BundleMatchers
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.Matchers.allOf
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.ext.waitNotNull
+
+class ShareOverlayRobot {
+
+ // This function verifies the share layout when more than one tab is shared - a list of tabs is shown
+ fun verifyShareTabsOverlay(vararg tabsTitles: String) {
+ Log.i(TAG, "verifyShareTabsOverlay: Trying to verify that the share overlay site list is displayed")
+ onView(withId(R.id.shared_site_list))
+ .check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabsOverlay: Verified that the share overlay site list is displayed")
+ for (tabs in tabsTitles) {
+ Log.i(TAG, "verifyShareTabsOverlay: Trying to verify the shared tab: $tabs favicon and url")
+ onView(withText(tabs))
+ .check(
+ matches(
+ allOf(
+ hasSibling(withId(R.id.share_tab_favicon)),
+ hasSibling(withId(R.id.share_tab_url)),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyShareTabsOverlay: Verified the shared tab: $tabs favicon and url")
+ }
+ }
+
+ // This function verifies the share layout when a single tab is shared - no tab info shown
+ fun verifyShareTabLayout() {
+ assertUIObjectExists(
+ // Share layout
+ itemWithResId("$packageName:id/sharingLayout"),
+ // Send to device section
+ itemWithResId("$packageName:id/devicesList"),
+ // Recently used section
+ itemWithResId("$packageName:id/recentAppsContainer"),
+ // All actions sections
+ itemWithResId("$packageName:id/appsList"),
+ // Send to device header
+ itemWithResIdContainingText(
+ "$packageName:id/accountHeaderText",
+ getStringResource(R.string.share_device_subheader),
+ ),
+ // Recently used header
+ itemWithResIdContainingText(
+ "$packageName:id/recent_apps_link_header",
+ getStringResource(R.string.share_link_recent_apps_subheader),
+ ),
+ // All actions header
+ itemWithResIdContainingText(
+ "$packageName:id/apps_link_header",
+ getStringResource(R.string.share_link_all_apps_subheader),
+ ),
+ // Save as PDF button
+ itemContainingText(getStringResource(R.string.share_save_to_pdf)),
+ )
+ }
+
+ // this verifies the Android sharing layout - not customized for sharing tabs
+ fun verifyAndroidShareLayout() {
+ mDevice.waitNotNull(Until.findObject(By.res("android:id/resolver_list")))
+ }
+
+ fun verifySharingWithSelectedApp(appName: String, content: String, subject: String) {
+ val sharingApp = mDevice.findObject(UiSelector().text(appName))
+ Log.i(TAG, "verifySharingWithSelectedApp: Trying to verify that sharing app: $appName exists")
+ if (sharingApp.exists()) {
+ Log.i(TAG, "verifySharingWithSelectedApp: Sharing app: $appName exists")
+ Log.i(TAG, "verifySharingWithSelectedApp: Trying to click sharing app: $appName and wait for a new window")
+ sharingApp.clickAndWaitForNewWindow()
+ Log.i(TAG, "verifySharingWithSelectedApp: Clicked sharing app: $appName and waited for a new window")
+ verifySharedTabsIntent(content, subject)
+ }
+ }
+
+ fun verifySharedTabsIntent(text: String, subject: String) {
+ Log.i(TAG, "verifySharedTabsIntent: Trying to verify the intent of the shared tab with text: $text, and subject: $subject")
+ Intents.intended(
+ allOf(
+ IntentMatchers.hasExtra(Intent.EXTRA_TEXT, text),
+ IntentMatchers.hasExtra(Intent.EXTRA_SUBJECT, subject),
+ ),
+ )
+ Log.i(TAG, "verifySharedTabsIntent: Verified the intent of the shared tab with text: $text, and subject: $subject")
+ }
+
+ fun verifyShareLinkIntent(url: Uri) {
+ Log.i(TAG, "verifyShareLinkIntent: Trying to verify that the share intent for link: $url is launched")
+ // verify share intent is launched and matched with associated passed in URL
+ Intents.intended(
+ allOf(
+ IntentMatchers.hasAction(Intent.ACTION_CHOOSER),
+ IntentMatchers.hasExtras(
+ allOf(
+ BundleMatchers.hasEntry(
+ Intent.EXTRA_INTENT,
+ allOf(
+ IntentMatchers.hasAction(Intent.ACTION_SEND),
+ IntentMatchers.hasType("text/plain"),
+ IntentMatchers.hasExtra(
+ Intent.EXTRA_TEXT,
+ url.toString(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyShareLinkIntent: Verified that the share intent for link: $url was launched")
+ }
+
+ class Transition {
+ fun clickSaveAsPDF(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
+ Log.i(TAG, "clickSaveAsPDF: Trying to click the \"SAVE AS PDF\" share overlay button")
+ itemContainingText("Save as PDF").click()
+ Log.i(TAG, "clickSaveAsPDF: Clicked the \"SAVE AS PDF\" share overlay button")
+
+ DownloadRobot().interact()
+ return DownloadRobot.Transition()
+ }
+
+ fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ itemWithText("Print").waitForExists(waitingTime)
+ Log.i(TAG, "clickPrintButton: Trying to click the \"Print\" share overlay button")
+ itemWithText("Print").click()
+ Log.i(TAG, "clickPrintButton: Clicked the \"Print\" share overlay button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+fun shareOverlay(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+}
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SitePermissionsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SitePermissionsRobot.kt
new file mode 100644
index 0000000000..9abbfd87f8
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SitePermissionsRobot.kt
@@ -0,0 +1,226 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.uiautomator.UiSelector
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+class SitePermissionsRobot {
+ fun verifyMicrophonePermissionPrompt(url: String) {
+ try {
+ assertUIObjectExists(itemWithText("Allow $url to use your microphone?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyMicrophonePermissionPrompt: AssertionError caught, executing fallback methods")
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }.clickStartMicrophoneButton {
+ assertUIObjectExists(itemWithText("Allow $url to use your microphone?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ }
+ }
+ }
+
+ fun verifyCameraPermissionPrompt(url: String) {
+ try {
+ assertUIObjectExists(itemWithText("Allow $url to use your camera?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyCameraPermissionPrompt: AssertionError caught, executing fallback methods")
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }.clickStartCameraButton {
+ assertUIObjectExists(itemWithText("Allow $url to use your camera?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ }
+ }
+ }
+
+ fun verifyAudioVideoPermissionPrompt(url: String) {
+ assertUIObjectExists(itemWithText("Allow $url to use your camera and microphone?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ }
+
+ fun verifyLocationPermissionPrompt(url: String) {
+ try {
+ assertUIObjectExists(itemWithText("Allow $url to use your location?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyLocationPermissionPrompt: AssertionError caught, executing fallback methods")
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }.clickGetLocationButton {
+ assertUIObjectExists(itemWithText("Allow $url to use your location?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ }
+ }
+ }
+
+ fun verifyNotificationsPermissionPrompt(url: String, blocked: Boolean = false) {
+ if (!blocked) {
+ try {
+ assertUIObjectExists(itemWithText("Allow $url to send notifications?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Never")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Always")
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyNotificationsPermissionPrompt: AssertionError caught, executing fallback methods")
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }.clickOpenNotificationButton {
+ assertUIObjectExists(itemWithText("Allow $url to send notifications?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Never")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Always")
+ }
+ }
+ } else {
+ /* if "Never" was selected in a previous step, or if the app is not allowed,
+ the Notifications permission prompt won't be displayed anymore */
+ assertUIObjectExists(itemWithText("Allow $url to send notifications?"), exists = false)
+ }
+ }
+
+ fun verifyPersistentStoragePermissionPrompt(url: String) {
+ try {
+ assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyPersistentStoragePermissionPrompt: AssertionError caught, executing fallback methods")
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }.clickRequestPersistentStorageAccessButton {
+ assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ }
+ }
+ }
+
+ fun verifyDRMContentPermissionPrompt(url: String) {
+ try {
+ assertUIObjectExists(itemWithText("Allow $url to play DRM-controlled content?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyDRMContentPermissionPrompt: AssertionError caught, executing fallback methods")
+ browserScreen {
+ }.openThreeDotMenu {
+ }.refreshPage {
+ }.clickRequestDRMControlledContentAccessButton {
+ assertUIObjectExists(itemWithText("Allow $url to play DRM-controlled content?"))
+ assertItemTextEquals(denyPagePermissionButton(), expectedText = "Don’t allow")
+ assertItemTextEquals(allowPagePermissionButton(), expectedText = "Allow")
+ }
+ }
+ }
+
+ fun verifyCrossOriginCookiesPermissionPrompt(originSite: String, currentSite: String) {
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Waiting for $waitingTime ms for \"Allow $originSite to use its cookies on $currentSite?\" prompt to exist")
+ mDevice.findObject(UiSelector().text("Allow $originSite to use its cookies on $currentSite?"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Waited for $waitingTime ms for \"Allow $originSite to use its cookies on $currentSite?\" prompt to exist")
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Trying to verify that the the storage access permission prompt title is displayed")
+ onView(ViewMatchers.withText("Allow $originSite to use its cookies on $currentSite?")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Verified that the the storage access permission prompt title is displayed")
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Trying to verify that the storage access permission prompt message is displayed")
+ onView(ViewMatchers.withText("You may want to block access if it's not clear why $originSite needs this data.")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Verified that the storage access permission prompt message is displayed")
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Trying to verify that the storage access permission prompt learn more link is displayed")
+ onView(ViewMatchers.withText("Learn more")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Verified that the storage access permission prompt learn more link is displayed")
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Trying to verify that the \"Block\" storage access permission prompt button is displayed")
+ onView(ViewMatchers.withText("Block")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Verified that the \"Block\" storage access permission prompt button is displayed")
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Trying to verify that the \"Allow\" storage access permission prompt button is displayed")
+ onView(ViewMatchers.withText("Allow")).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyCrossOriginCookiesPermissionPrompt: Verified that the \"Allow\" storage access permission prompt button is displayed")
+ }
+
+ fun selectRememberPermissionDecision() {
+ Log.i(TAG, "selectRememberPermissionDecision: Waiting for $waitingTime ms for the \"Remember decision for this site\" check box to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/do_not_ask_again"))
+ .waitForExists(waitingTime)
+ Log.i(TAG, "selectRememberPermissionDecision: Waited for $waitingTime ms for the \"Remember decision for this site\" check box to exist")
+ Log.i(TAG, "selectRememberPermissionDecision: Trying to click the \"Remember decision for this site\" check box")
+ onView(withId(R.id.do_not_ask_again))
+ .check(matches(isDisplayed()))
+ .click()
+ Log.i(TAG, "selectRememberPermissionDecision: Clicked the \"Remember decision for this site\" check box")
+ }
+
+ class Transition {
+ fun clickPagePermissionButton(allow: Boolean, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ if (allow) {
+ Log.i(TAG, "clickPagePermissionButton: Waiting for $waitingTime ms for the \"Allow\" prompt button to exist")
+ allowPagePermissionButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickPagePermissionButton: Waited for $waitingTime ms for the \"Allow\" prompt button to exist")
+ Log.i(TAG, "clickPagePermissionButton: Trying to click the \"Allow\" prompt button")
+ allowPagePermissionButton().click()
+ Log.i(TAG, "clickPagePermissionButton: Clicked the \"Allow\" prompt button")
+ // sometimes flaky, the prompt is not dismissed, retrying
+ Log.i(TAG, "clickPagePermissionButton: Waiting for $waitingTime ms for the \"Allow\" prompt button to be gone")
+ if (!allowPagePermissionButton().waitUntilGone(waitingTime)) {
+ Log.i(TAG, "clickPagePermissionButton: The \"Allow\" prompt button is not gone")
+ Log.i(TAG, "clickPagePermissionButton: Trying to click again the \"Allow\" prompt button")
+ allowPagePermissionButton().click()
+ Log.i(TAG, "clickPagePermissionButton: Clicked again the \"Allow\" prompt button")
+ }
+ Log.i(TAG, "clickPagePermissionButton: Waited for $waitingTime ms for the \"Allow\" prompt button to be gone")
+ } else {
+ Log.i(TAG, "clickPagePermissionButton: Waiting for $waitingTime ms for the \"Don’t allow\" prompt button to exist")
+ denyPagePermissionButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickPagePermissionButton: Waited for $waitingTime ms for the \"Don’t allow\" prompt button to exist")
+ Log.i(TAG, "clickPagePermissionButton: Trying to click the \"Don’t allow\" prompt button")
+ denyPagePermissionButton().click()
+ Log.i(TAG, "clickPagePermissionButton: Clicked the \"Don’t allow\" prompt button")
+ Log.i(TAG, "clickPagePermissionButton: Waiting for $waitingTime ms for the \"Don’t allow\" prompt button to be gone")
+ // sometimes flaky, the prompt is not dismissed, retrying
+ if (!denyPagePermissionButton().waitUntilGone(waitingTime)) {
+ Log.i(TAG, "clickPagePermissionButton: The \"Don’t allow\" prompt button is not gone")
+ Log.i(TAG, "clickPagePermissionButton: Trying to click again the \"Don’t allow\" prompt button")
+ denyPagePermissionButton().click()
+ Log.i(TAG, "clickPagePermissionButton: Clicked again the \"Don’t allow\" prompt button")
+ }
+ Log.i(TAG, "clickPagePermissionButton: Waited for $waitingTime ms for the \"Don’t allow\" prompt button to be gone")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+// Page permission prompts buttons
+private fun allowPagePermissionButton() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/allow_button"))
+
+private fun denyPagePermissionButton() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/deny_button"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SiteSecurityRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SiteSecurityRobot.kt
new file mode 100644
index 0000000000..7a7684b705
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SiteSecurityRobot.kt
@@ -0,0 +1,172 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.uiautomator.UiSelector
+import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+
+/**
+ * Implementation of Robot Pattern for Site Security UI.
+ */
+class SiteSecurityRobot {
+
+ fun verifyQuickActionSheet(url: String = "", isConnectionSecure: Boolean) {
+ Log.i(TAG, "verifyQuickActionSheet: Waiting for $waitingTime ms for quick action sheet to exist")
+ quickActionSheet().waitForExists(waitingTime)
+ Log.i(TAG, "verifyQuickActionSheet: Waited for $waitingTime ms for quick action sheet to exist")
+ assertUIObjectExists(
+ quickActionSheetUrl(url.tryGetHostFromUrl()),
+ quickActionSheetSecurityInfo(isConnectionSecure),
+ quickActionSheetTrackingProtectionSwitch(),
+ quickActionSheetClearSiteData(),
+ )
+ }
+ fun openSecureConnectionSubMenu(isConnectionSecure: Boolean) {
+ Log.i(TAG, "openSecureConnectionSubMenu: Trying to click the security info button while connection is secure: $isConnectionSecure")
+ quickActionSheetSecurityInfo(isConnectionSecure).click()
+ Log.i(TAG, "openSecureConnectionSubMenu: Clicked the security info button while connection is secure: $isConnectionSecure")
+ Log.i(TAG, "openSecureConnectionSubMenu: Trying to click the security info button and wait for $waitingTimeShort ms for a new window")
+ mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
+ Log.i(TAG, "openSecureConnectionSubMenu: Clicked the security info button and waited for $waitingTimeShort ms for a new window")
+ }
+ fun verifySecureConnectionSubMenu(pageTitle: String = "", url: String = "", isConnectionSecure: Boolean) {
+ Log.i(TAG, "verifySecureConnectionSubMenu: Waiting for $waitingTime ms for secure connection submenu to exist")
+ secureConnectionSubMenu().waitForExists(waitingTime)
+ Log.i(TAG, "verifySecureConnectionSubMenu: Waited for $waitingTime ms for secure connection submenu to exist")
+ assertUIObjectExists(
+ secureConnectionSubMenuPageTitle(pageTitle),
+ secureConnectionSubMenuPageUrl(url),
+ secureConnectionSubMenuSecurityInfo(isConnectionSecure),
+ secureConnectionSubMenuLockIcon(),
+ secureConnectionSubMenuCertificateInfo(),
+ )
+ }
+ fun clickQuickActionSheetClearSiteData() {
+ Log.i(TAG, "clickQuickActionSheetClearSiteData: Trying to click the \"Clear cookies and site data\" button")
+ quickActionSheetClearSiteData().click()
+ Log.i(TAG, "clickQuickActionSheetClearSiteData: Clicked the \"Clear cookies and site data\" button")
+ }
+ fun verifyClearSiteDataPrompt(url: String) {
+ assertUIObjectExists(clearSiteDataPrompt(url))
+ Log.i(TAG, "verifyClearSiteDataPrompt: Trying to verify that the \"Cancel\" dialog button is displayed")
+ cancelClearSiteDataButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearSiteDataPrompt: Verified that the \"Cancel\" dialog button is displayed")
+ Log.i(TAG, "verifyClearSiteDataPrompt: Trying to verify that the \"Delete\" dialog button is displayed")
+ deleteSiteDataButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyClearSiteDataPrompt: Verified that the \"Delete\" dialog button is displayed")
+ }
+
+ class Transition
+}
+
+private fun quickActionSheet() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/quick_action_sheet"))
+
+private fun quickActionSheetUrl(url: String) =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/url")
+ .textContains(url),
+ )
+
+private fun quickActionSheetSecurityInfo(isConnectionSecure: Boolean) =
+ if (isConnectionSecure) {
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/securityInfo")
+ .textContains(getStringResource(R.string.quick_settings_sheet_secure_connection_2)),
+ )
+ } else {
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/securityInfo")
+ .textContains(getStringResource(R.string.quick_settings_sheet_insecure_connection_2)),
+ )
+ }
+
+private fun quickActionSheetTrackingProtectionSwitch() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/trackingProtectionSwitch"),
+ )
+
+private fun quickActionSheetClearSiteData() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/clearSiteData"),
+ )
+
+private fun secureConnectionSubMenu() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/design_bottom_sheet"),
+ )
+
+private fun secureConnectionSubMenuPageTitle(pageTitle: String) =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/title")
+ .textContains(pageTitle),
+ )
+
+private fun secureConnectionSubMenuPageUrl(url: String) =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/url")
+ .textContains(url),
+ )
+
+private fun secureConnectionSubMenuLockIcon() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/securityInfoIcon"),
+ )
+
+private fun secureConnectionSubMenuSecurityInfo(isConnectionSecure: Boolean) =
+ if (isConnectionSecure) {
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/securityInfo")
+ .textContains(getStringResource(R.string.quick_settings_sheet_secure_connection_2)),
+ )
+ } else {
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/securityInfo")
+ .textContains(getStringResource(R.string.quick_settings_sheet_insecure_connection_2)),
+ )
+ }
+
+private fun secureConnectionSubMenuCertificateInfo() =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/securityInfo"),
+ )
+
+private fun clearSiteDataPrompt(url: String) =
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("android:id/message")
+ .textContains(url),
+ )
+
+private fun cancelClearSiteDataButton() = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog())
+private fun deleteSiteDataButton() = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog())
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt
new file mode 100644
index 0000000000..b86f63a8c7
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt
@@ -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.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.uiautomator.UiSelector
+import org.hamcrest.CoreMatchers.allOf
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for Sync Sign In sub menu.
+ */
+class SyncSignInRobot {
+
+ fun verifyTurnOnSyncMenu() {
+ Log.i(TAG, "verifyTurnOnSyncMenu: Waiting for $waitingTime ms for sign in to sync menu to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime)
+ Log.i(TAG, "verifyTurnOnSyncMenu: Waited for $waitingTime ms for sign in to sync menu to exist")
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/signInScanButton"),
+ itemWithResId("$packageName:id/signInEmailButton"),
+ )
+ }
+
+ class Transition {
+ fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click the navigate up button")
+ goBackButton().click()
+ Log.i(TAG, "goBack: Clicked the navigate up button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+
+private fun goBackButton() =
+ onView(allOf(withContentDescription("Navigate up")))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt
new file mode 100644
index 0000000000..c99133c334
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.uiautomator.UiSelector
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+
+class SystemSettingsRobot {
+
+ fun verifyNotifications() {
+ Log.i(TAG, "verifyNotifications: Trying to verify the intent to the notifications settings")
+ Intents.intended(hasAction("android.settings.APP_NOTIFICATION_SETTINGS"))
+ Log.i(TAG, "verifyNotifications: Verified the intent to the notifications settings")
+ }
+
+ fun verifyMakeDefaultBrowser() {
+ Log.i(TAG, "verifyMakeDefaultBrowser: Trying to verify the intent to the default apps settings")
+ Intents.intended(hasAction(SettingsRobot.DEFAULT_APPS_SETTINGS_ACTION))
+ Log.i(TAG, "verifyMakeDefaultBrowser: Verified the intent to the default apps settings")
+ }
+
+ fun verifyAllSystemNotificationsToggleState(enabled: Boolean) {
+ if (enabled) {
+ Log.i(TAG, "verifyAllSystemNotificationsToggleState: Trying to verify that the system settings \"Show notifications\" toggle is checked")
+ assertTrue("$TAG: The system settings \"Show notifications\" toggle is not checked", allSystemSettingsNotificationsToggle().isChecked)
+ Log.i(TAG, "verifyAllSystemNotificationsToggleState: Verified that the system settings \"Show notifications\" toggle is checked")
+ } else {
+ Log.i(TAG, "verifyAllSystemNotificationsToggleState: Trying to verify that the system settings \"Show notifications\" toggle is not checked")
+ assertFalse("$TAG: The system settings \"Show notifications\" toggle is checked", allSystemSettingsNotificationsToggle().isChecked)
+ Log.i(TAG, "verifyAllSystemNotificationsToggleState: Verified that the system settings \"Show notifications\" toggle is not checked")
+ }
+ }
+
+ fun verifyPrivateBrowsingSystemNotificationsToggleState(enabled: Boolean) {
+ if (enabled) {
+ Log.i(TAG, "verifyPrivateBrowsingSystemNotificationsToggleState: Trying to verify that the system settings \"Private browsing session\" toggle is checked")
+ assertTrue("$TAG: The system settings \"Private browsing sessio\" toggle is not checked", privateBrowsingSystemSettingsNotificationsToggle().isChecked)
+ Log.i(TAG, "verifyPrivateBrowsingSystemNotificationsToggleState: Verified that the system settings \"Private browsing session\" toggle is checked")
+ } else {
+ Log.i(TAG, "verifyPrivateBrowsingSystemNotificationsToggleState: Trying to verify that the system settings \"Private browsing session\" toggle is not checked")
+ assertFalse("$TAG: The system settings \"Private browsing session\" toggle is checked", privateBrowsingSystemSettingsNotificationsToggle().isChecked)
+ Log.i(TAG, "verifyPrivateBrowsingSystemNotificationsToggleState: Verified that the system settings \"Private browsing session\" toggle is not checked")
+ }
+ }
+
+ fun clickPrivateBrowsingSystemNotificationsToggle() {
+ Log.i(TAG, "clickPrivateBrowsingSystemNotificationsToggle: Trying to click the system settings \"Private browsing session\" toggle")
+ privateBrowsingSystemSettingsNotificationsToggle().click()
+ Log.i(TAG, "clickPrivateBrowsingSystemNotificationsToggle: Clicked the system settings \"Private browsing session\" toggle")
+ }
+
+ fun clickAllSystemNotificationsToggle() {
+ Log.i(TAG, "clickAllSystemNotificationsToggle: Trying to click the system settings \"Show notifications\" toggle")
+ allSystemSettingsNotificationsToggle().click()
+ Log.i(TAG, "clickAllSystemNotificationsToggle: Clicked the system settings \"Show notifications\" toggle")
+ }
+
+ class Transition {
+ // Difficult to know where this will go
+ fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
+ Log.i(TAG, "goBack: Trying to click device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "goBack: Clicked device back button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+ }
+}
+
+fun systemSettings(interact: SystemSettingsRobot.() -> Unit): SystemSettingsRobot.Transition {
+ SystemSettingsRobot().interact()
+ return SystemSettingsRobot.Transition()
+}
+
+private fun allSystemSettingsNotificationsToggle() =
+ mDevice.findObject(
+ UiSelector().resourceId("com.android.settings:id/switch_bar")
+ .childSelector(
+ UiSelector()
+ .resourceId("com.android.settings:id/switch_widget")
+ .index(1),
+ ),
+ )
+private fun privateBrowsingSystemSettingsNotificationsToggle() =
+ itemWithResIdAndDescription("com.android.settings:id/switchWidget", "Private browsing session")
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt
new file mode 100644
index 0000000000..250b69f85d
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt
@@ -0,0 +1,687 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import android.view.View
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.action.GeneralLocation
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.swipeLeft
+import androidx.test.espresso.action.ViewActions.swipeRight
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.By.text
+import androidx.test.uiautomator.UiScrollable
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until.findObject
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.anyOf
+import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.Matcher
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
+import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.clickAtLocationInView
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource
+import org.mozilla.fenix.helpers.isSelected
+import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher
+import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorStateMatcher
+
+/**
+ * Implementation of Robot Pattern for the home screen menu.
+ */
+class TabDrawerRobot {
+
+ fun verifyNormalBrowsingButtonIsSelected(isSelected: Boolean) {
+ Log.i(TAG, "verifyNormalBrowsingButtonIsSelected: Trying to verify that the normal browsing button is selected: $isSelected")
+ normalBrowsingButton().check(matches(isSelected(isSelected)))
+ Log.i(TAG, "verifyNormalBrowsingButtonIsSelected: Verified that the normal browsing button is selected: $isSelected")
+ }
+
+ fun verifyPrivateBrowsingButtonIsSelected(isSelected: Boolean) {
+ Log.i(TAG, "verifyPrivateBrowsingButtonIsSelected: Trying to verify that the private browsing button is selected: $isSelected")
+ privateBrowsingButton().check(matches(isSelected(isSelected)))
+ Log.i(TAG, "verifyPrivateBrowsingButtonIsSelected: Verified that the private browsing button is selected: $isSelected")
+ }
+
+ fun verifySyncedTabsButtonIsSelected(isSelected: Boolean) {
+ Log.i(TAG, "verifySyncedTabsButtonIsSelected: Trying to verify that the synced tabs button is selected: $isSelected")
+ syncedTabsButton().check(matches(isSelected(isSelected)))
+ Log.i(TAG, "verifySyncedTabsButtonIsSelected: Verified that the synced tabs button is selected: $isSelected")
+ }
+
+ fun clickSyncedTabsButton() {
+ Log.i(TAG, "clickSyncedTabsButton: Trying to click the synced tabs button")
+ syncedTabsButton().click()
+ Log.i(TAG, "clickSyncedTabsButton: Clicked the synced tabs button")
+ }
+
+ fun verifyExistingOpenTabs(vararg tabTitles: String) {
+ var retries = 0
+
+ for (title in tabTitles) {
+ while (!tabItem(title).waitForExists(waitingTime) && retries++ < 3) {
+ tabsList()
+ .getChildByText(UiSelector().text(title), title, true)
+ assertUIObjectExists(tabItem(title), waitingTime = waitingTimeLong)
+ }
+ }
+ }
+
+ fun verifyOpenTabsOrder(position: Int, title: String) {
+ Log.i(TAG, "verifyOpenTabsOrder: Trying to verify that the open tab at position: $position has title: $title")
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/tab_item")
+ .childSelector(
+ UiSelector().textContains(title),
+ ),
+ ).getFromParent(
+ UiSelector()
+ .resourceId("$packageName:id/tab_tray_grid_item")
+ .index(position - 1),
+ )
+ Log.i(TAG, "verifyOpenTabsOrder: Verified that the open tab at position: $position has title: $title")
+ }
+ fun verifyNoExistingOpenTabs(vararg tabTitles: String) {
+ for (title in tabTitles) {
+ assertUIObjectExists(tabItem(title), exists = false)
+ }
+ }
+ fun verifyCloseTabsButton(title: String) =
+ assertUIObjectExists(itemWithDescription("Close tab").getFromParent(UiSelector().textContains(title)))
+
+ fun verifyExistingTabList() {
+ Log.i(TAG, "verifyExistingTabList: Waiting for $waitingTime ms for tab tray to exist")
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/tabsTray"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "verifyExistingTabList: Waited for $waitingTime ms for tab tray to exist")
+ assertUIObjectExists(itemWithResId("$packageName:id/tray_list_item"))
+ }
+
+ fun verifyNoOpenTabsInNormalBrowsing() {
+ Log.i(TAG, "verifyNoOpenTabsInNormalBrowsing: Trying to verify that the empty normal tabs list is visible")
+ onView(
+ allOf(
+ withId(R.id.tab_tray_empty_view),
+ withText(R.string.no_open_tabs_description),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyNoOpenTabsInNormalBrowsing: Verified that the empty normal tabs list is visible")
+ }
+
+ fun verifyNoOpenTabsInPrivateBrowsing() {
+ Log.i(TAG, "verifyNoOpenTabsInPrivateBrowsing: Trying to verify that the empty private tabs list is visible")
+ onView(
+ allOf(
+ withId(R.id.tab_tray_empty_view),
+ withText(R.string.no_private_tabs_description),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyNoOpenTabsInPrivateBrowsing: Verified that the empty private tabs list is visible")
+ }
+
+ fun verifyPrivateModeSelected() {
+ Log.i(TAG, "verifyPrivateModeSelected: Trying to verify that the private browsing button is selected")
+ privateBrowsingButton().check(matches(ViewMatchers.isSelected()))
+ Log.i(TAG, "verifyPrivateModeSelected: Verified that the private browsing button is selected")
+ }
+
+ fun verifyNormalModeSelected() {
+ Log.i(TAG, "verifyNormalModeSelected: Trying to verify that the normal browsing button is selected")
+ normalBrowsingButton().check(matches(ViewMatchers.isSelected()))
+ Log.i(TAG, "verifyNormalModeSelected: Verified that the normal browsing button is selected")
+ }
+
+ fun verifyNormalBrowsingNewTabButton() {
+ Log.i(TAG, "verifyNormalBrowsingNewTabButton: Trying to verify that the new tab FAB button is visible")
+ onView(
+ allOf(
+ withId(R.id.new_tab_button),
+ withContentDescription(R.string.add_tab),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyNormalBrowsingNewTabButton: Verified that the new tab FAB button is visible")
+ }
+
+ fun verifyPrivateBrowsingNewTabButton() {
+ Log.i(TAG, "verifyPrivateBrowsingNewTabButton: Trying to verify that the new private tab FAB button is visible")
+ onView(
+ allOf(
+ withId(R.id.new_tab_button),
+ withContentDescription(R.string.add_private_tab),
+ ),
+ ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyPrivateBrowsingNewTabButton: Verified that the new private tab FAB button is visible")
+ }
+
+ fun verifyEmptyTabsTrayMenuButtons() {
+ Log.i(TAG, "verifyEmptyTabsTrayMenuButtons: Trying to click the three dot button")
+ threeDotMenu().click()
+ Log.i(TAG, "verifyEmptyTabsTrayMenuButtons: Clicked the three dot button")
+ Log.i(TAG, "verifyEmptyTabsTrayMenuButtons: Trying to verify that the \"Tab settings\" menu button is visible")
+ tabsSettingsButton()
+ .inRoot(RootMatchers.isPlatformPopup())
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEmptyTabsTrayMenuButtons: Verified that the \"Tab settings\" menu button is visible")
+ Log.i(TAG, "verifyEmptyTabsTrayMenuButtons: Trying to verify that the \"Recently closed tabs\" menu button is visible")
+ recentlyClosedTabsButton()
+ .inRoot(RootMatchers.isPlatformPopup())
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEmptyTabsTrayMenuButtons: Verified that the \"Recently closed tabs\" menu button exists")
+ }
+
+ fun verifyTabTrayOverflowMenu(visibility: Boolean) {
+ Log.i(TAG, "verifyTabTrayOverflowMenu: Trying to verify that the three dot menu is visible: $visibility")
+ onView(withId(R.id.tab_tray_overflow)).check(
+ matches(
+ withEffectiveVisibility(
+ visibleOrGone(
+ visibility,
+ ),
+ ),
+ ),
+ )
+ Log.i(TAG, "verifyTabTrayOverflowMenu: Verified that the three dot menu is visible: $visibility")
+ }
+ fun verifyTabsTrayCounter() {
+ Log.i(TAG, "verifyTabsTrayCounter: Trying to verify that the tabs list counter is visible")
+ tabsTrayCounterBox().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyTabsTrayCounter: Verified that the tabs list counter is visible")
+ }
+
+ fun verifyTabTrayIsOpened() {
+ Log.i(TAG, "verifyTabTrayIsOpened: Trying to verify that the tabs tray is visible")
+ onView(withId(R.id.tab_wrapper)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ Log.i(TAG, "verifyTabTrayIsOpened: Verified that the tabs tray is visible")
+ }
+ fun verifyTabTrayIsClosed() {
+ Log.i(TAG, "verifyTabTrayIsClosed: Trying to verify that the tabs tray does not exist")
+ onView(withId(R.id.tab_wrapper)).check(doesNotExist())
+ Log.i(TAG, "verifyTabTrayIsClosed: Verified that the tabs tray does not exist")
+ }
+ fun verifyHalfExpandedRatio() {
+ Log.i(TAG, "verifyHalfExpandedRatio: Trying to verify the tabs tray half expanded ratio")
+ onView(withId(R.id.tab_wrapper)).check(
+ matches(
+ BottomSheetBehaviorHalfExpandedMaxRatioMatcher(0.001f),
+ ),
+ )
+ Log.i(TAG, "verifyHalfExpandedRatio: Verified the tabs tray half expanded ratio")
+ }
+ fun verifyBehaviorState(expectedState: Int) {
+ Log.i(TAG, "verifyBehaviorState: Trying to verify that the tabs tray state matches: $expectedState")
+ onView(withId(R.id.tab_wrapper)).check(matches(BottomSheetBehaviorStateMatcher(expectedState)))
+ Log.i(TAG, "verifyBehaviorState: Verified that the tabs tray state matches: $expectedState")
+ }
+ fun verifyOpenedTabThumbnail() =
+ assertUIObjectExists(itemWithResId("$packageName:id/mozac_browser_tabstray_thumbnail"))
+
+ fun closeTab() {
+ Log.i(TAG, "closeTab: Waiting for $waitingTime ms for the close tab button to exist")
+ closeTabButton().waitForExists(waitingTime)
+ Log.i(TAG, "closeTab: Waited for $waitingTime ms for the close tab button to exist")
+
+ var retries = 0 // number of retries before failing, will stop at 2
+ do {
+ Log.i(TAG, "closeTab: Trying to click the close tab button")
+ closeTabButton().click()
+ Log.i(TAG, "closeTab: Clicked the close tab button")
+ retries++
+ } while (closeTabButton().exists() && retries < 3)
+ }
+
+ fun closeTabWithTitle(title: String) {
+ itemWithResIdAndDescription(
+ "$packageName:id/mozac_browser_tabstray_close",
+ "Close tab $title",
+ ).also {
+ Log.i(TAG, "closeTabWithTitle: Waiting for $waitingTime ms for the close button for tab with title: $title to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "closeTabWithTitle: Waited for $waitingTime ms for the close button for tab with title: $title to exist")
+ Log.i(TAG, "closeTabWithTitle: Trying to click the close button for tab with title: $title")
+ it.click()
+ Log.i(TAG, "closeTabWithTitle: Clicked the close button for tab with title: $title")
+ }
+ }
+
+ fun swipeTabRight(title: String) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "swipeTabRight: Started try #$i")
+ try {
+ Log.i(TAG, "swipeTabRight: Trying to perform swipe right action on tab: $title")
+ onView(
+ allOf(
+ withId(R.id.tab_item),
+ hasDescendant(
+ allOf(
+ withId(R.id.mozac_browser_tabstray_title),
+ withText(title),
+ ),
+ ),
+ ),
+ ).perform(swipeRight())
+ Log.i(TAG, "swipeTabRight: Performed swipe right action on tab: $title")
+ assertUIObjectIsGone(
+ itemWithResIdContainingText(
+ "$packageName:id/mozac_browser_tabstray_title",
+ title,
+ ),
+ )
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "swipeTabRight: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ }
+ }
+ }
+ }
+
+ fun swipeTabLeft(title: String) {
+ for (i in 1..RETRY_COUNT) {
+ try {
+ Log.i(TAG, "swipeTabLeft: Trying to perform swipe left action on tab: $title")
+ onView(
+ allOf(
+ withId(R.id.tab_item),
+ hasDescendant(
+ allOf(
+ withId(R.id.mozac_browser_tabstray_title),
+ withText(title),
+ ),
+ ),
+ ),
+ ).perform(swipeLeft())
+ Log.i(TAG, "swipeTabLeft: Performed swipe left action on tab: $title")
+ assertUIObjectIsGone(
+ itemWithResIdContainingText(
+ "$packageName:id/mozac_browser_tabstray_title",
+ title,
+ ),
+ )
+
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "swipeTabLeft: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ }
+ }
+ }
+ }
+
+ fun verifyTabMediaControlButtonState(action: String) = assertUIObjectExists(tabMediaControlButton(action))
+
+ fun clickTabMediaControlButton(action: String) {
+ tabMediaControlButton(action).also {
+ Log.i(TAG, "clickTabMediaControlButton: Waiting for $waitingTime ms for the media tab control button: $action to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickTabMediaControlButton: Waited for $waitingTime ms for the media tab control button: $action to exists")
+ Log.i(TAG, "clickTabMediaControlButton: Trying to click the tab media control button: $action")
+ it.click()
+ Log.i(TAG, "clickTabMediaControlButton: Clicked the tab media control button: $action")
+ }
+ }
+
+ fun clickSelectTabsOption() {
+ Log.i(TAG, "clickSelectTabsOption: Trying to click the three dot button")
+ threeDotMenu().click()
+ Log.i(TAG, "clickSelectTabsOption: Clicked the three dot button")
+ mDevice.findObject(UiSelector().text("Select tabs")).also {
+ Log.i(TAG, "clickSelectTabsOption: Waiting for $waitingTime ms for the the \"Select tabs\" menu button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "clickSelectTabsOption: Waited for $waitingTime ms for the the \"Select tabs\" menu button to exists")
+ Log.i(TAG, "clickSelectTabsOption: Trying to click the \"Select tabs\" menu button")
+ it.click()
+ Log.i(TAG, "clickSelectTabsOption: Clicked the \"Select tabs\" menu button")
+ }
+ }
+
+ fun selectTab(title: String, numOfTabs: Int) {
+ val tabsSelected =
+ mDevice.findObject(UiSelector().text("$numOfTabs selected"))
+ var retries = 0 // number of retries before failing
+
+ while (!tabsSelected.exists() && retries++ < 3) {
+ Log.i(TAG, "selectTab: Waiting for $waitingTime ms for tab with title: $title to exist")
+ tabItem(title).waitForExists(waitingTime)
+ Log.i(TAG, "selectTab: Waited for $waitingTime ms for tab with title: $title to exist")
+ Log.i(TAG, "selectTab: Trying to click tab with title: $title")
+ tabItem(title).click()
+ Log.i(TAG, "selectTab: Clicked tab with title: $title")
+ }
+ }
+
+ fun longClickTab(title: String) {
+ mDevice.waitNotNull(
+ findObject(text(title)),
+ waitingTime,
+ )
+ Log.i(TAG, "longClickTab: Trying to long click tab with title: $title")
+ mDevice.findObject(
+ By
+ .textContains(title)
+ .res("$packageName:id/mozac_browser_tabstray_title"),
+ ).click(LONG_CLICK_DURATION)
+ Log.i(TAG, "longClickTab: Long clicked tab with title: $title")
+ }
+
+ fun createCollection(
+ vararg tabTitles: String,
+ collectionName: String,
+ firstCollection: Boolean = true,
+ ) {
+ tabDrawer {
+ clickSelectTabsOption()
+ for (tab in tabTitles) {
+ selectTab(tab, tabTitles.indexOf(tab) + 1)
+ }
+ }.clickSaveCollection {
+ if (!firstCollection) {
+ clickAddNewCollection()
+ }
+ typeCollectionNameAndSave(collectionName)
+ }
+ }
+
+ fun verifyTabsMultiSelectionCounter(numOfTabs: Int) =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/multiselect_title"),
+ itemContainingText("$numOfTabs selected"),
+ )
+
+ fun verifySyncedTabsListWhenUserIsNotSignedIn() =
+ assertUIObjectExists(
+ itemWithResId("$packageName:id/tabsTray"),
+ itemContainingText(getStringResource(R.string.synced_tabs_sign_in_message)),
+ itemContainingText(getStringResource(R.string.sync_sign_in)),
+ )
+
+ class Transition {
+ fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "openTabDrawer: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "openTabDrawer: Waited for device to be idle for $waitingTime ms")
+ Log.i(TAG, "openTabDrawer: Trying to click the tab counter button")
+ tabsCounter().click()
+ Log.i(TAG, "openTabDrawer: Clicked the tab counter button")
+ mDevice.waitNotNull(
+ findObject(By.res("$packageName:id/tab_layout")),
+ waitingTime,
+ )
+
+ TabDrawerRobot().interact()
+ return Transition()
+ }
+
+ fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeTabDrawer: Waiting for device to be idle for $waitingTime ms")
+ mDevice.waitForIdle(waitingTime)
+ Log.i(TAG, "closeTabDrawer: Waited for device to be idle for $waitingTime ms")
+ Log.i(TAG, "closeTabDrawer: Trying to click the tabs tray handler")
+ onView(withId(R.id.handle)).perform(
+ click(),
+ )
+ Log.i(TAG, "closeTabDrawer: Clicked the tabs tray handler")
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "openNewTab: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "openNewTab: Waited for device to be idle")
+ Log.i(TAG, "openNewTab: Trying to click the new tab FAB button")
+ newTabButton().click()
+ Log.i(TAG, "openNewTab: Clicked the new tab FAB button")
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+
+ fun toggleToNormalTabs(interact: TabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "toggleToNormalTabs: Trying to click the normal browsing button")
+ normalBrowsingButton().perform(click())
+ Log.i(TAG, "toggleToNormalTabs: Clicked the normal browsing button")
+ TabDrawerRobot().interact()
+ return Transition()
+ }
+
+ fun toggleToPrivateTabs(interact: TabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "toggleToPrivateTabs: Trying to click the private browsing button")
+ privateBrowsingButton().perform(click())
+ Log.i(TAG, "toggleToPrivateTabs: Clicked the private browsing button")
+ TabDrawerRobot().interact()
+ return Transition()
+ }
+
+ fun toggleToSyncedTabs(interact: TabDrawerRobot.() -> Unit): Transition {
+ Log.i(TAG, "toggleToSyncedTabs: Trying to click the synced tabs button")
+ syncedTabsButton().perform(click())
+ Log.i(TAG, "toggleToSyncedTabs: Clicked the synced tabs button")
+ TabDrawerRobot().interact()
+ return Transition()
+ }
+
+ fun clickSignInToSyncButton(interact: SyncSignInRobot.() -> Unit): Transition {
+ Log.i(TAG, "clickSignInToSyncButton: Trying to click the sign in to sync button and wait for $waitingTimeShort ms for a new window")
+ itemContainingText(getStringResource(R.string.sync_sign_in))
+ .clickAndWaitForNewWindow(waitingTimeShort)
+ Log.i(TAG, "clickSignInToSyncButton: Clicked the sign in to sync button and waited for $waitingTimeShort ms for a new window")
+ SyncSignInRobot().interact()
+ return Transition()
+ }
+
+ fun openTabsListThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
+ Log.i(TAG, "openTabsListThreeDotMenu: Trying to click the three dot button")
+ threeDotMenu().perform(click())
+ Log.i(TAG, "openTabsListThreeDotMenu: Clicked three dot button")
+
+ ThreeDotMenuMainRobot().interact()
+ return ThreeDotMenuMainRobot.Transition()
+ }
+
+ fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ scrollToElementByText(title)
+ Log.i(TAG, "openTab: Waiting for $waitingTime ms for tab with title: $title to exist")
+ tabItem(title).waitForExists(waitingTime)
+ Log.i(TAG, "openTab: Waited for $waitingTime ms for tab with title: $title to exist")
+ Log.i(TAG, "openTab: Trying to click tab with title: $title")
+ tabItem(title).click()
+ Log.i(TAG, "openTab: Clicked tab with title: $title")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ // Temporary method to use indexes instead of tab titles, until the compose migration is complete
+ fun openTabWithIndex(
+ tabPosition: Int,
+ interact: BrowserRobot.() -> Unit,
+ ): BrowserRobot.Transition {
+ mDevice.findObject(
+ UiSelector()
+ .resourceId("$packageName:id/tab_tray_grid_item")
+ .index(tabPosition),
+ ).also {
+ Log.i(TAG, "openTabWithIndex: Waiting for $waitingTime ms for tab at position: ${tabPosition + 1} to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "openTabWithIndex: Waited for $waitingTime ms for tab at position: ${tabPosition + 1} to exist")
+ Log.i(TAG, "openTabWithIndex: Trying to click tab at position: ${tabPosition + 1}")
+ it.click()
+ Log.i(TAG, "openTabWithIndex: Clicked tab at position: ${tabPosition + 1}")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickTopBar(interact: TabDrawerRobot.() -> Unit): Transition {
+ // The topBar contains other views.
+ // Don't do the default click in the middle, rather click in some free space - top right.
+ Log.i(TAG, "clickTopBar: Trying to click the tabs tray top bar")
+ onView(withId(R.id.topBar)).clickAtLocationInView(GeneralLocation.TOP_RIGHT)
+ Log.i(TAG, "clickTopBar: Clicked the tabs tray top bar")
+ TabDrawerRobot().interact()
+ return Transition()
+ }
+
+ fun advanceToHalfExpandedState(interact: TabDrawerRobot.() -> Unit): Transition {
+ onView(withId(R.id.tab_wrapper)).perform(
+ object : ViewAction {
+ override fun getDescription(): String {
+ return "Advance a BottomSheetBehavior to STATE_HALF_EXPANDED"
+ }
+
+ override fun getConstraints(): Matcher {
+ return ViewMatchers.isAssignableFrom(View::class.java)
+ }
+
+ override fun perform(uiController: UiController?, view: View?) {
+ val behavior = BottomSheetBehavior.from(view!!)
+ behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
+ }
+ },
+ )
+ TabDrawerRobot().interact()
+ return Transition()
+ }
+
+ fun waitForTabTrayBehaviorToIdle(interact: TabDrawerRobot.() -> Unit): Transition {
+ // Need to get the behavior of tab_wrapper and wait for that to idle.
+ var behavior: BottomSheetBehavior<*>? = null
+
+ // Null check here since it's possible that the view is already animated away from the screen.
+ onView(withId(R.id.tab_wrapper))?.perform(
+ object : ViewAction {
+ override fun getDescription(): String {
+ return "Postpone actions to after the BottomSheetBehavior has settled"
+ }
+
+ override fun getConstraints(): Matcher {
+ return ViewMatchers.isAssignableFrom(View::class.java)
+ }
+
+ override fun perform(uiController: UiController?, view: View?) {
+ behavior = BottomSheetBehavior.from(view!!)
+ }
+ },
+ )
+
+ behavior?.let {
+ registerAndCleanupIdlingResources(
+ BottomSheetBehaviorStateIdlingResource(it),
+ ) {
+ TabDrawerRobot().interact()
+ }
+ }
+
+ return Transition()
+ }
+
+ fun clickSaveCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
+ Log.i(TAG, "clickSaveCollection: Trying to click the collections button")
+ saveTabsToCollectionButton().click()
+ Log.i(TAG, "clickSaveCollection: Clicked the collections button")
+
+ CollectionRobot().interact()
+ return CollectionRobot.Transition()
+ }
+ }
+}
+
+fun tabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+}
+
+private fun tabMediaControlButton(action: String) =
+ mDevice.findObject(UiSelector().descriptionContains(action))
+
+private fun closeTabButton() =
+ mDevice.findObject(UiSelector().descriptionContains("Close tab"))
+
+private fun normalBrowsingButton() = onView(
+ anyOf(
+ withContentDescription(containsString("open tabs. Tap to switch tabs.")),
+ withContentDescription(containsString("open tab. Tap to switch tabs.")),
+ ),
+)
+
+private fun privateBrowsingButton() = onView(withContentDescription("Private tabs"))
+private fun syncedTabsButton() = onView(withContentDescription("Synced tabs"))
+private fun newTabButton() =
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/new_tab_button"))
+
+private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))
+
+private fun tabsList() =
+ UiScrollable(UiSelector().className("androidx.recyclerview.widget.RecyclerView"))
+
+// This tab selector is used for actions that involve waiting and asserting the existence of the view
+private fun tabItem(title: String) =
+ mDevice.findObject(
+ UiSelector()
+ .textContains(title),
+ )
+
+private fun tabsCounter() = onView(withId(R.id.tab_button))
+
+private fun tabsTrayCounterBox() = onView(withId(R.id.counter_box))
+
+private fun tabsSettingsButton() =
+ onView(
+ allOf(
+ withId(R.id.simple_text),
+ withText(R.string.tab_tray_menu_tab_settings),
+ ),
+ )
+
+private fun recentlyClosedTabsButton() =
+ onView(
+ allOf(
+ withId(R.id.simple_text),
+ withText(R.string.tab_tray_menu_recently_closed),
+ ),
+ )
+
+private fun visibleOrGone(visibility: Boolean) =
+ if (visibility) ViewMatchers.Visibility.VISIBLE else ViewMatchers.Visibility.GONE
+
+private fun saveTabsToCollectionButton() = onView(withId(R.id.collect_multi_select))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuBookmarksRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuBookmarksRobot.kt
new file mode 100644
index 0000000000..e5cae2d880
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuBookmarksRobot.kt
@@ -0,0 +1,147 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
+import org.mozilla.fenix.helpers.click
+
+/**
+ * Implementation of Robot Pattern for the Bookmarks three dot menu.
+ */
+class ThreeDotMenuBookmarksRobot {
+
+ class Transition {
+
+ fun clickEdit(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ Log.i(TAG, "clickEdit: Trying to click the \"Edit\" button")
+ editButton().click()
+ Log.i(TAG, "clickEdit: Clicked the \"Edit\" button")
+
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+ }
+
+ fun clickCopy(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ Log.i(TAG, "clickCopy: Trying to click the \"Copy\" button")
+ copyButton().click()
+ Log.i(TAG, "clickCopy: Clicked the \"Copy\" button")
+
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+ }
+
+ fun clickShare(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ Log.i(TAG, "clickShare: Trying to click the \"Share\" button")
+ shareButton().click()
+ Log.i(TAG, "clickShare: Clicked the \"Share\" button")
+
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+ }
+
+ fun clickOpenInNewTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenInNewTab: Trying to click the \"Open in new tab\" button")
+ openInNewTabButton().click()
+ Log.i(TAG, "clickOpenInNewTab: Clicked the \"Open in new tab\" button")
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickOpenInNewTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenInNewTab: Trying to click the \"Open in new tab\" button")
+ openInNewTabButton().click()
+ Log.i(TAG, "clickOpenInNewTab: Clicked the \"Open in new tab\" button")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun clickOpenInPrivateTab(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenInPrivateTab: Trying to click the \"Open in private tab\" button")
+ openInPrivateTabButton().click()
+ Log.i(TAG, "clickOpenInPrivateTab: Clicked the \"Open in private tab\" button")
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickOpenInPrivateTab(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenInPrivateTab: Trying to click the \"Open in private tab\" button")
+ openInPrivateTabButton().click()
+ Log.i(TAG, "clickOpenInPrivateTab: Clicked the \"Open in private tab\" button")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun clickOpenAllInTabs(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenAllInTabs: Trying to click the \"Open all in new tabs\" button")
+ openAllInTabsButton().click()
+ Log.i(TAG, "clickOpenAllInTabs: Clicked the \"Open all in new tabs\" button")
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickOpenAllInTabs(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenAllInTabs: Trying to click the \"Open all in new tabs\" button")
+ openAllInTabsButton().click()
+ Log.i(TAG, "clickOpenAllInTabs: Clicked the \"Open all in new tabs\" button")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun clickOpenAllInPrivateTabs(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenAllInPrivateTabs: Trying to click the \"Open all in private tabs\" button")
+ openAllInPrivateTabsButton().click()
+ Log.i(TAG, "clickOpenAllInPrivateTabs: Clicked the \"Open all in private tabs\" button")
+
+ TabDrawerRobot().interact()
+ return TabDrawerRobot.Transition()
+ }
+
+ fun clickOpenAllInPrivateTabs(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
+ Log.i(TAG, "clickOpenAllInPrivateTabs: Trying to click the \"Open all in private tabs\" button")
+ openAllInPrivateTabsButton().click()
+ Log.i(TAG, "clickOpenAllInPrivateTabs: Clicked the \"Open all in private tabs\" button")
+
+ ComposeTabDrawerRobot(composeTestRule).interact()
+ return ComposeTabDrawerRobot.Transition(composeTestRule)
+ }
+
+ fun clickDelete(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ Log.i(TAG, "clickDelete: Trying to click the \"Delete\" button")
+ deleteButton().click()
+ Log.i(TAG, "clickDelete: Clicked the \"Delete\" button")
+
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+ }
+ }
+}
+
+private fun editButton() = onView(withText("Edit"))
+
+private fun copyButton() = onView(withText("Copy"))
+
+private fun shareButton() = onView(withText("Share"))
+
+private fun openInNewTabButton() = onView(withText("Open in new tab"))
+
+private fun openInPrivateTabButton() = onView(withText("Open in private tab"))
+
+private fun openAllInTabsButton() = onView(withText("Open all in new tabs"))
+
+private fun openAllInPrivateTabsButton() = onView(withText("Open all in private tabs"))
+
+private fun deleteButton() = onView(withText("Delete"))
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt
new file mode 100644
index 0000000000..a20a79088a
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt
@@ -0,0 +1,758 @@
+/* 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("TooManyFunctions")
+
+package org.mozilla.fenix.ui.robots
+
+import android.util.Log
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.swipeDown
+import androidx.test.espresso.action.ViewActions.swipeUp
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObjectNotFoundException
+import androidx.test.uiautomator.UiSelector
+import androidx.test.uiautomator.Until
+import org.hamcrest.Matchers.allOf
+import org.junit.Assert.assertTrue
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
+import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
+import org.mozilla.fenix.helpers.Constants.TAG
+import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
+import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
+import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
+import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
+import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
+import org.mozilla.fenix.helpers.TestHelper.mDevice
+import org.mozilla.fenix.helpers.TestHelper.packageName
+import org.mozilla.fenix.helpers.click
+import org.mozilla.fenix.helpers.ext.waitNotNull
+import org.mozilla.fenix.nimbus.FxNimbus
+
+/**
+ * Implementation of Robot Pattern for the three dot (main) menu.
+ */
+@Suppress("ForbiddenComment")
+class ThreeDotMenuMainRobot {
+ fun verifyShareAllTabsButton() {
+ Log.i(TAG, "verifyShareAllTabsButton: Trying to verify that the \"Share all tabs\" menu button is displayed")
+ shareAllTabsButton().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareAllTabsButton: Verified that the \"Share all tabs\" menu button is displayed")
+ }
+ fun verifySettingsButton() = assertUIObjectExists(settingsButton())
+ fun verifyHistoryButton() = assertUIObjectExists(historyButton())
+ fun verifyThreeDotMenuExists() {
+ Log.i(TAG, "verifyThreeDotMenuExists: Trying to verify that the three dot menu is displayed")
+ threeDotMenuRecyclerView().check(matches(isDisplayed()))
+ Log.i(TAG, "verifyThreeDotMenuExists: Verified that the three dot menu is displayed")
+ }
+ fun verifyAddBookmarkButton() = assertUIObjectExists(addBookmarkButton())
+ fun verifyEditBookmarkButton() {
+ Log.i(TAG, "verifyEditBookmarkButton: Trying to verify that the \"Edit\" button is visible")
+ editBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyEditBookmarkButton: Verified that the \"Edit\" button is visible")
+ }
+ fun verifyCloseAllTabsButton() {
+ Log.i(TAG, "verifyCloseAllTabsButton: Trying to verify that the \"Close all tabs\" button is visible")
+ closeAllTabsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyCloseAllTabsButton: Verified that the \"Close all tabs\" button is visible")
+ }
+ fun verifyReaderViewAppearance(visible: Boolean) {
+ var maxSwipes = 3
+ if (visible) {
+ while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
+ Log.i(TAG, "verifyReaderViewAppearance: The \"Customize reader view\" button does not exist")
+ Log.i(TAG, "verifyReaderViewAppearance: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "verifyReaderViewAppearance: Performed swipe up action on the three dot menu")
+ maxSwipes--
+ }
+ assertUIObjectExists(readerViewAppearanceToggle())
+ } else {
+ while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
+ Log.i(TAG, "verifyReaderViewAppearance: The \"Customize reader view\" button does not exist")
+ Log.i(TAG, "verifyReaderViewAppearance: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "verifyReaderViewAppearance: Performed swipe up action on the three dot menu")
+ maxSwipes--
+ }
+ assertUIObjectExists(readerViewAppearanceToggle(), exists = false)
+ }
+ }
+
+ fun verifyQuitButtonExists() {
+ // Need to double swipe the menu, to make this button visible.
+ // In case it reaches the end, the second swipe is no-op.
+ expandMenu()
+ expandMenu()
+ assertUIObjectExists(itemWithText("Quit"))
+ }
+
+ fun expandMenu() {
+ Log.i(TAG, "expandMenu: Trying to perform swipe up action on the three dot menu")
+ onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
+ Log.i(TAG, "expandMenu: Performed swipe up action on the three dot menu")
+ }
+
+ fun verifyShareTabButton() {
+ Log.i(TAG, "verifyShareTabButton: Trying to verify that the \"Share all tabs\" button is visible")
+ shareTabButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyShareTabButton: Verified that the \"Share all tabs\" button is visible")
+ }
+ fun verifySelectTabs() {
+ Log.i(TAG, "verifySelectTabs: Trying to verify that the \"Select tabs\" button is visible")
+ selectTabsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifySelectTabs: Verified that the \"Select tabs\" button is visible")
+ }
+
+ fun verifyFindInPageButton() = assertUIObjectExists(findInPageButton())
+ fun verifyAddToShortcutsButton(shouldExist: Boolean) =
+ assertUIObjectExists(addToShortcutsButton(), exists = shouldExist)
+ fun verifyRemoveFromShortcutsButton() {
+ Log.i(TAG, "verifyRemoveFromShortcutsButton: Trying to perform scroll action to the \"Settings\" button")
+ onView(withId(R.id.mozac_browser_menu_recyclerView))
+ .perform(
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(R.string.browser_menu_settings)),
+ ),
+ ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ Log.i(TAG, "verifyRemoveFromShortcutsButton: Performed scroll action to the \"Settings\" button")
+ }
+
+ fun verifyShareTabsOverlay() {
+ Log.i(TAG, "verifyShareTabsOverlay: Trying to verify that the share overlay site list is displayed")
+ onView(withId(R.id.shared_site_list)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabsOverlay: Verified that the share overlay site list is displayed")
+ Log.i(TAG, "verifyShareTabsOverlay: Trying to verify that the shared tab title is displayed")
+ onView(withId(R.id.share_tab_title)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabsOverlay: Verified that the shared tab title is displayed")
+ Log.i(TAG, "verifyShareTabsOverlay: Trying to verify that the shared tab favicon is displayed")
+ onView(withId(R.id.share_tab_favicon)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabsOverlay: Verified that the shared tab favicon is displayed")
+ Log.i(TAG, "verifyShareTabsOverlay: Trying to verify that the shared tab url is displayed")
+ onView(withId(R.id.share_tab_url)).check(matches(isDisplayed()))
+ Log.i(TAG, "verifyShareTabsOverlay: Verified that the shared tab url is displayed")
+ }
+
+ fun verifyDesktopSiteModeEnabled(isRequestDesktopSiteEnabled: Boolean) {
+ expandMenu()
+ assertUIObjectExists(desktopSiteToggle(isRequestDesktopSiteEnabled))
+ }
+
+ fun verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
+ expandMenu()
+ assertUIObjectExists(
+ normalBrowsingNewTabButton(),
+ bookmarksButton(),
+ historyButton(),
+ downloadsButton(),
+ addOnsButton(),
+ syncAndSaveDataButton(),
+ findInPageButton(),
+ desktopSiteButton(),
+ reportSiteIssueButton(),
+ addToHomeScreenButton(),
+ addToShortcutsButton(),
+ saveToCollectionButton(),
+ addBookmarkButton(),
+ desktopSiteToggle(isRequestDesktopSiteEnabled),
+ translateButton(),
+ )
+ // Swipe to second part of menu
+ expandMenu()
+ assertUIObjectExists(
+ settingsButton(),
+ )
+ if (FxNimbus.features.print.value().browserPrintEnabled) {
+ assertUIObjectExists(printContentButton())
+ }
+ assertUIObjectExists(
+ backButton(),
+ forwardButton(),
+ shareButton(),
+ refreshButton(),
+ )
+ }
+
+ fun verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
+ assertUIObjectExists(
+ bookmarksButton(),
+ historyButton(),
+ downloadsButton(),
+ addOnsButton(),
+ // Disabled step due to https://github.com/mozilla-mobile/fenix/issues/26788
+ // syncAndSaveDataButton,
+ desktopSiteButton(),
+ whatsNewButton(),
+ helpButton(),
+ customizeHomeButton(),
+ settingsButton(),
+ desktopSiteToggle(isRequestDesktopSiteEnabled),
+ )
+ }
+
+ fun openAddonsSubList() {
+ // when there are add-ons installed, there is an overflow Add-ons sub-menu
+ // in that case we use this method instead or before openAddonsManagerMenu()
+ clickAddonsManagerButton()
+ }
+
+ fun verifyAddonAvailableInMainMenu(addonName: String) {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "verifyAddonAvailableInMainMenu: Started try #$i")
+ try {
+ assertUIObjectExists(itemContainingText(addonName))
+ break
+ } catch (e: AssertionError) {
+ Log.i(TAG, "verifyAddonAvailableInMainMenu: AssertionError caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ mDevice.pressBack()
+ browserScreen {
+ }.openThreeDotMenu {
+ openAddonsSubList()
+ }
+ }
+ }
+ }
+ }
+
+ fun verifyTrackersBlockedByUblock() {
+ assertUIObjectExists(itemWithResId("$packageName:id/badge_text"))
+ Log.i(TAG, "verifyTrackersBlockedByUblock: Trying to verify that the count of trackers blocked is greater than 0")
+ assertTrue("$TAG: The count of trackers blocked is not greater than 0", itemWithResId("$packageName:id/badge_text").text.toInt() > 0)
+ Log.i(TAG, "verifyTrackersBlockedByUblock: Verified that the count of trackers blocked is greater than 0")
+ }
+
+ fun clickQuit() {
+ expandMenu()
+ Log.i(TAG, "clickQuit: Trying to click the \"Quit\" button")
+ onView(withText("Quit")).click()
+ Log.i(TAG, "clickQuit: Clicked the \"Quit\" button")
+ }
+
+ class Transition {
+ fun openSettings(
+ localizedText: String = getStringResource(R.string.browser_menu_settings),
+ interact: SettingsRobot.() -> Unit,
+ ): SettingsRobot.Transition {
+ // We require one swipe to display the full size 3-dot menu. On smaller devices
+ // such as the Pixel 2, we require two swipes to display the "Settings" menu item
+ // at the bottom. On larger devices, the second swipe is a no-op.
+ Log.i(TAG, "openSettings: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openSettings: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openSettings: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openSettings: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openSettings: Trying to click the $localizedText button")
+ settingsButton(localizedText).click()
+ Log.i(TAG, "openSettings: Clicked the $localizedText button")
+
+ SettingsRobot().interact()
+ return SettingsRobot.Transition()
+ }
+
+ fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
+ Log.i(TAG, "openDownloadsManager: Trying to perform swipe down action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeDown())
+ Log.i(TAG, "openDownloadsManager: Performed swipe down action on the three dot menu")
+ Log.i(TAG, "openDownloadsManager: Trying to click the \"DOWNLOADS\" button")
+ downloadsButton().click()
+ Log.i(TAG, "openDownloadsManager: Clicked the \"DOWNLOADS\" button")
+
+ DownloadRobot().interact()
+ return DownloadRobot.Transition()
+ }
+
+ fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
+ Log.i(TAG, "openSyncSignIn: Trying to perform swipe down action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeDown())
+ Log.i(TAG, "openSyncSignIn: Performed swipe down action on the three dot menu")
+ mDevice.waitNotNull(Until.findObject(By.text("Sync and save data")), waitingTime)
+ Log.i(TAG, "openSyncSignIn: Trying to click the \"Sync and save data\" button")
+ syncAndSaveDataButton().click()
+ Log.i(TAG, "openSyncSignIn: Clicked the \"Sync and save data\" button")
+
+ SyncSignInRobot().interact()
+ return SyncSignInRobot.Transition()
+ }
+
+ fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ Log.i(TAG, "openBookmarks: Trying to perform swipe down action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeDown())
+ Log.i(TAG, "openBookmarks: Performed swipe down action on the three dot menu")
+ mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
+ Log.i(TAG, "openBookmarks: Trying to click the \"Bookmarks\" button")
+ bookmarksButton().click()
+ Log.i(TAG, "openBookmarks: Clicked the \"Bookmarks\" button")
+ assertUIObjectExists(itemWithResId("$packageName:id/bookmark_list"))
+
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+ }
+
+ fun clickNewTabButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
+ Log.i(TAG, "clickNewTabButton: Trying to click the \"New tab\" button")
+ normalBrowsingNewTabButton().click()
+ Log.i(TAG, "clickNewTabButton: Clicked the \"New tab\" button")
+
+ SearchRobot().interact()
+ return SearchRobot.Transition()
+ }
+
+ fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
+ Log.i(TAG, "openHistory: Trying to perform swipe down action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeDown())
+ Log.i(TAG, "openHistory: Performed swipe down action on the three dot menu")
+ mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime)
+ Log.i(TAG, "openHistory: Trying to click the \"History\" button")
+ historyButton().click()
+ Log.i(TAG, "openHistory: Clicked the \"History\" button")
+
+ HistoryRobot().interact()
+ return HistoryRobot.Transition()
+ }
+
+ fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
+ Log.i(TAG, "bookmarkPage: Trying to click the \"Add\" button")
+ addBookmarkButton().click()
+ Log.i(TAG, "bookmarkPage: Clicked the \"Add\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun editBookmarkPage(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
+ mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
+ Log.i(TAG, "editBookmarkPage: Trying to click the \"Edit\" button")
+ editBookmarkButton().click()
+ Log.i(TAG, "editBookmarkPage: Clicked the \"Edit\" button")
+
+ BookmarksRobot().interact()
+ return BookmarksRobot.Transition()
+ }
+
+ fun openHelp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ mDevice.waitNotNull(Until.findObject(By.text("Help")), waitingTime)
+ Log.i(TAG, "openHelp: Trying to click the \"Help\" button")
+ helpButton().click()
+ Log.i(TAG, "openHelp: Clicked the \"Help\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openCustomizeHome(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
+ Log.i(TAG, "openCustomizeHome: Waiting for $waitingTime ms until finding the \"Customize homepage\" button")
+ mDevice.wait(
+ Until
+ .findObject(
+ By.textContains("$packageName:id/browser_menu_customize_home_1"),
+ ),
+ waitingTime,
+ )
+ Log.i(TAG, "openCustomizeHome: Waited for $waitingTime ms until the \"Customize homepage\" button was found")
+ Log.i(TAG, "openCustomizeHome: Trying to click the \"Customize homepage\" button")
+ customizeHomeButton().click()
+ Log.i(TAG, "openCustomizeHome: Clicked the \"Customize homepage\" button")
+ Log.i(TAG, "openCustomizeHome: Waiting for $waitingTime ms for \"Customize homepage\" settings menu to exist")
+ mDevice.findObject(
+ UiSelector().resourceId("$packageName:id/recycler_view"),
+ ).waitForExists(waitingTime)
+ Log.i(TAG, "openCustomizeHome: Waited for $waitingTime ms for \"Customize homepage\" settings menu to exist")
+
+ SettingsSubMenuHomepageRobot().interact()
+ return SettingsSubMenuHomepageRobot.Transition()
+ }
+
+ fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goForward: Trying to click the \"Forward\" button")
+ forwardButton().click()
+ Log.i(TAG, "goForward: Clicked the \"Forward\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun goToPreviousPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "goToPreviousPage: Trying to click the \"Back\" button")
+ backButton().click()
+ Log.i(TAG, "goToPreviousPage: Clicked the \"Back\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ Log.i(TAG, "clickShareButton: Trying to click the \"Share\" button")
+ shareButton().click()
+ Log.i(TAG, "clickShareButton: Clicked the \"Share\" button")
+ mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
+
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+ }
+
+ fun closeBrowserMenuToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "closeBrowserMenuToBrowser: Trying to click device back button")
+ // Close three dot
+ mDevice.pressBack()
+ Log.i(TAG, "closeBrowserMenuToBrowser: Clicked the device back button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ refreshButton().also {
+ Log.i(TAG, "refreshPage: Waiting for $waitingTime ms for the \"Refresh\" button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "refreshPage: Waited for $waitingTime ms for the \"Refresh\" button to exist")
+ Log.i(TAG, "refreshPage: Trying to click the \"Refresh\" button")
+ it.click()
+ Log.i(TAG, "refreshPage: Clicked the \"Refresh\" button")
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun forceRefreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "forceRefreshPage: Trying to long click the \"Refresh\" button")
+ mDevice.findObject(By.desc(getStringResource(R.string.browser_menu_refresh)))
+ .click(LONG_CLICK_DURATION)
+ Log.i(TAG, "forceRefreshPage: Long clicked the \"Refresh\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun closeAllTabs(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
+ Log.i(TAG, "closeAllTabs: Trying to click the \"Close all tabs\" button")
+ closeAllTabsButton().click()
+ Log.i(TAG, "closeAllTabs: Clicked the \"Close all tabs\" button")
+
+ HomeScreenRobot().interact()
+ return HomeScreenRobot.Transition()
+ }
+
+ fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "openReportSiteIssue: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openReportSiteIssue: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openReportSiteIssue: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openReportSiteIssue: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openReportSiteIssue: Trying to click the \"Report Site Issue\" button")
+ reportSiteIssueButton().click()
+ Log.i(TAG, "openReportSiteIssue: Clicked the \"Report Site Issue\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition {
+ Log.i(TAG, "openFindInPage: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openFindInPage: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openFindInPage: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openFindInPage: Performed swipe up action on the three dot menu")
+ mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
+ Log.i(TAG, "openFindInPage: Trying to click the \"Find in page\" button")
+ findInPageButton().click()
+ Log.i(TAG, "openFindInPage: Clicked the \"Find in page\" button")
+
+ FindInPageRobot().interact()
+ return FindInPageRobot.Transition()
+ }
+
+ fun openWhatsNew(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ mDevice.waitNotNull(Until.findObject(By.text("What’s new")), waitingTime)
+ Log.i(TAG, "openWhatsNew: Trying to click the \"What’s new\" button")
+ whatsNewButton().click()
+ Log.i(TAG, "openWhatsNew: Clicked the \"What’s new\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
+ Log.i(TAG, "openReaderViewAppearance: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openReaderViewAppearance: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openReaderViewAppearance: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openReaderViewAppearance: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openReaderViewAppearance: Trying to click the \"Customize reader view\" button")
+ readerViewAppearanceToggle().click()
+ Log.i(TAG, "openReaderViewAppearance: Clicked the \"Customize reader view\" button")
+
+ ReaderViewRobot().interact()
+ return ReaderViewRobot.Transition()
+ }
+
+ fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ for (i in 1..RETRY_COUNT) {
+ Log.i(TAG, "addToFirefoxHome: Started try #$i")
+ try {
+ addToShortcutsButton().also {
+ Log.i(TAG, "addToFirefoxHome: Waiting for $waitingTime ms for the \"Add to shortcuts\" button to exist")
+ it.waitForExists(waitingTime)
+ Log.i(TAG, "addToFirefoxHome: Waited for $waitingTime ms for the \"Add to shortcuts\" button to exist")
+ Log.i(TAG, "addToFirefoxHome: Trying to click the \"Add to shortcuts\" button")
+ it.click()
+ Log.i(TAG, "addToFirefoxHome: Clicked the \"Add to shortcuts\" button")
+ }
+
+ break
+ } catch (e: UiObjectNotFoundException) {
+ Log.i(TAG, "addToFirefoxHome: UiObjectNotFoundException caught, executing fallback methods")
+ if (i == RETRY_COUNT) {
+ throw e
+ } else {
+ Log.i(TAG, "addToFirefoxHome: Trying to click the device back button")
+ mDevice.pressBack()
+ Log.i(TAG, "addToFirefoxHome: Clicked the device back button")
+ navigationToolbar {
+ }.openThreeDotMenu {
+ expandMenu()
+ }
+ }
+ }
+ }
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickRemoveFromShortcuts(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickRemoveFromShortcuts: Trying to click the \"Remove from shortcuts\" button")
+ removeFromShortcutsButton().click()
+ Log.i(TAG, "clickRemoveFromShortcuts: Clicked the \"Remove from shortcuts\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun openAddToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
+ Log.i(TAG, "openAddToHomeScreen: Trying to click the \"Add to Home screen\" button and wait for $waitingTime ms for a new window")
+ addToHomeScreenButton().clickAndWaitForNewWindow(waitingTime)
+ Log.i(TAG, "openAddToHomeScreen: Clicked the \"Add to Home screen\" button and waited for $waitingTime ms for a new window")
+
+ AddToHomeScreenRobot().interact()
+ return AddToHomeScreenRobot.Transition()
+ }
+
+ fun clickInstall(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
+ Log.i(TAG, "clickInstall: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "clickInstall: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "clickInstall: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "clickInstall: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "clickInstall: Trying to click the \"Install\" button")
+ installPWAButton().click()
+ Log.i(TAG, "clickInstall: Clicked the \"Install\" button")
+
+ AddToHomeScreenRobot().interact()
+ return AddToHomeScreenRobot.Transition()
+ }
+
+ fun openSaveToCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
+ // Ensure the menu is expanded and fully scrolled to the bottom.
+ Log.i(TAG, "openSaveToCollection: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openSaveToCollection: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "openSaveToCollection: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "openSaveToCollection: Performed swipe up action on the three dot menu")
+
+ mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
+ Log.i(TAG, "openSaveToCollection: Trying to click the \"Save to collection\" button")
+ saveToCollectionButton().click()
+ Log.i(TAG, "openSaveToCollection: Clicked the \"Save to collection\" button")
+ CollectionRobot().interact()
+ return CollectionRobot.Transition()
+ }
+
+ fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
+ clickAddonsManagerButton()
+ Log.i(TAG, "openAddonsManagerMenu: Waiting for $waitingTimeLong ms for the addons list to exist")
+ mDevice.findObject(UiSelector().resourceId("$packageName:id/add_ons_list"))
+ .waitForExists(waitingTimeLong)
+ Log.i(TAG, "openAddonsManagerMenu: Waited for $waitingTimeLong ms for the addons list to exist")
+
+ SettingsSubMenuAddonsManagerRobot().interact()
+ return SettingsSubMenuAddonsManagerRobot.Transition()
+ }
+
+ fun clickOpenInApp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickOpenInApp: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "clickOpenInApp: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "clickOpenInApp: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "clickOpenInApp: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "clickOpenInApp: Trying to click the \"Open in app\" button")
+ openInAppButton().click()
+ Log.i(TAG, "clickOpenInApp: Clicked the \"Open in app\" button")
+ Log.i(TAG, "clickOpenInApp: Waiting for device to be idle")
+ mDevice.waitForIdle()
+ Log.i(TAG, "clickOpenInApp: Waited for device to be idle")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "switchDesktopSiteMode: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "switchDesktopSiteMode: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "switchDesktopSiteMode: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "switchDesktopSiteMode: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "switchDesktopSiteMode: Trying to click the \"Desktop site\" button")
+ desktopSiteButton().click()
+ Log.i(TAG, "switchDesktopSiteMode: Clicked the \"Desktop site\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+
+ fun clickShareAllTabsButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
+ Log.i(TAG, "clickShareAllTabsButton: Trying to click the \"Share all tabs\" button")
+ shareAllTabsButton().click()
+ Log.i(TAG, "clickShareAllTabsButton: Clicked the \"Share all tabs\" button")
+
+ ShareOverlayRobot().interact()
+ return ShareOverlayRobot.Transition()
+ }
+
+ fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
+ Log.i(TAG, "clickPrintButton: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "clickPrintButton: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "clickPrintButton: Trying to perform swipe up action on the three dot menu")
+ threeDotMenuRecyclerView().perform(swipeUp())
+ Log.i(TAG, "clickPrintButton: Performed swipe up action on the three dot menu")
+ Log.i(TAG, "clickPrintButton: Waiting for $waitingTime ms for the \"Print\" button to exist")
+ printButton().waitForExists(waitingTime)
+ Log.i(TAG, "clickPrintButton: Waited for $waitingTime ms for the \"Print\" button to exist")
+ Log.i(TAG, "clickPrintButton: Trying to click the \"Print\" button")
+ printButton().click()
+ Log.i(TAG, "clickPrintButton: Clicked the \"Print\" button")
+
+ BrowserRobot().interact()
+ return BrowserRobot.Transition()
+ }
+ }
+}
+private fun threeDotMenuRecyclerView() =
+ onView(withId(R.id.mozac_browser_menu_recyclerView))
+
+private fun editBookmarkButton() = onView(withText("Edit"))
+
+private fun stopLoadingButton() = onView(ViewMatchers.withContentDescription("Stop"))
+
+private fun closeAllTabsButton() = onView(allOf(withText("Close all tabs"))).inRoot(RootMatchers.isPlatformPopup())
+
+private fun shareTabButton() = onView(allOf(withText("Share all tabs"))).inRoot(RootMatchers.isPlatformPopup())
+
+private fun selectTabsButton() = onView(allOf(withText("Select tabs"))).inRoot(RootMatchers.isPlatformPopup())
+
+private fun readerViewAppearanceToggle() =
+ mDevice.findObject(UiSelector().text("Customize reader view"))
+
+private fun removeFromShortcutsButton() =
+ onView(allOf(withText(R.string.browser_menu_remove_from_shortcuts)))
+
+private fun installPWAButton() =
+ itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen))
+
+private fun openInAppButton() =
+ onView(
+ allOf(
+ withText("Open in app"),
+ withEffectiveVisibility(Visibility.VISIBLE),
+ ),
+ )
+
+private fun clickAddonsManagerButton() {
+ Log.i(TAG, "clickAddonsManagerButton: Trying to perform swipe down action on the three dot menu")
+ onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
+ Log.i(TAG, "clickAddonsManagerButton: Performed swipe down action on the three dot menu")
+ Log.i(TAG, "clickAddonsManagerButton: Trying to click the \"Add-ons\" button")
+ addOnsButton().click()
+ Log.i(TAG, "clickAddonsManagerButton: Clicked the \"Add-ons\" button")
+}
+
+private fun shareAllTabsButton() =
+ onView(allOf(withText("Share all tabs"))).inRoot(RootMatchers.isPlatformPopup())
+
+private fun bookmarksButton() =
+ itemContainingText(getStringResource(R.string.library_bookmarks))
+private fun historyButton() =
+ itemContainingText(getStringResource(R.string.library_history))
+private fun downloadsButton() =
+ itemContainingText(getStringResource(R.string.library_downloads))
+private fun addOnsButton() =
+ itemContainingText(getStringResource(R.string.browser_menu_extensions))
+private fun desktopSiteButton() =
+ itemContainingText(getStringResource(R.string.browser_menu_desktop_site))
+private fun desktopSiteToggle(state: Boolean) =
+ checkedItemWithResIdAndText(
+ "$packageName:id/switch_widget",
+ getStringResource(R.string.browser_menu_desktop_site),
+ state,
+ )
+private fun whatsNewButton() =
+ itemContainingText(getStringResource(R.string.browser_menu_whats_new))
+private fun helpButton() =
+ itemContainingText(getStringResource(R.string.browser_menu_help))
+private fun customizeHomeButton() =
+ itemContainingText(getStringResource(R.string.browser_menu_customize_home_1))
+private fun settingsButton(localizedText: String = getStringResource(R.string.browser_menu_settings)) =
+ itemContainingText(localizedText)
+private fun syncAndSaveDataButton() =
+ itemContainingText(getStringResource(R.string.sync_menu_sync_and_save_data))
+private fun normalBrowsingNewTabButton() =
+ itemContainingText(getStringResource(R.string.library_new_tab))
+private fun addBookmarkButton() =
+ itemWithResIdAndText(
+ "$packageName:id/checkbox",
+ getStringResource(R.string.browser_menu_add),
+ )
+private fun findInPageButton() = itemContainingText(getStringResource(R.string.browser_menu_find_in_page))
+private fun translateButton() = itemContainingText(getStringResource(R.string.browser_menu_translations))
+private fun reportSiteIssueButton() = itemContainingText("Report Site Issue")
+private fun addToHomeScreenButton() = itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen))
+private fun addToShortcutsButton() = itemContainingText(getStringResource(R.string.browser_menu_add_to_shortcuts))
+private fun saveToCollectionButton() = itemContainingText(getStringResource(R.string.browser_menu_save_to_collection_2))
+private fun printContentButton() = itemContainingText(getStringResource(R.string.menu_print))
+private fun backButton() = itemWithDescription(getStringResource(R.string.browser_menu_back))
+private fun forwardButton() = itemWithDescription(getStringResource(R.string.browser_menu_forward))
+private fun shareButton() = itemWithDescription(getStringResource(R.string.share_button_content_description))
+private fun refreshButton() = itemWithDescription(getStringResource(R.string.browser_menu_refresh))
+private fun printButton() = itemWithText("Print")
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt
new file mode 100644
index 0000000000..bc9544f720
--- /dev/null
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ui.util
+
+const val FRENCH_LANGUAGE_HEADER = "Langues"
+const val ROMANIAN_LANGUAGE_HEADER = "Limbă"
+const val ARABIC_LANGUAGE_HEADER = "اللغة"
+const val FR_SETTINGS = "Paramètres"
+const val FRENCH_SYSTEM_LOCALE_OPTION = "Utiliser la langue de l’appareil"
diff --git a/mobile/android/fenix/app/src/androidTest/resources/email.txt b/mobile/android/fenix/app/src/androidTest/resources/email.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mobile/android/fenix/app/src/androidTest/resources/password.txt b/mobile/android/fenix/app/src/androidTest/resources/password.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mobile/android/fenix/app/src/beta/AndroidManifest.xml b/mobile/android/fenix/app/src/beta/AndroidManifest.xml
new file mode 100644
index 0000000000..4207f1fbce
--- /dev/null
+++ b/mobile/android/fenix/app/src/beta/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-hdpi/fenix_search_widget.webp b/mobile/android/fenix/app/src/beta/res/drawable-hdpi/fenix_search_widget.webp
new file mode 100644
index 0000000000..b6938e5661
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-hdpi/fenix_search_widget.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-hdpi/ic_logo_wordmark_normal.webp b/mobile/android/fenix/app/src/beta/res/drawable-hdpi/ic_logo_wordmark_normal.webp
new file mode 100644
index 0000000000..34ec5df3f8
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-hdpi/ic_logo_wordmark_normal.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-hdpi/ic_logo_wordmark_private.webp b/mobile/android/fenix/app/src/beta/res/drawable-hdpi/ic_logo_wordmark_private.webp
new file mode 100644
index 0000000000..22c7f96f17
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-hdpi/ic_logo_wordmark_private.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-mdpi/ic_logo_wordmark_normal.webp b/mobile/android/fenix/app/src/beta/res/drawable-mdpi/ic_logo_wordmark_normal.webp
new file mode 100644
index 0000000000..cbd0caf055
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-mdpi/ic_logo_wordmark_normal.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-mdpi/ic_logo_wordmark_private.webp b/mobile/android/fenix/app/src/beta/res/drawable-mdpi/ic_logo_wordmark_private.webp
new file mode 100644
index 0000000000..cab5f8e345
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-mdpi/ic_logo_wordmark_private.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-xhdpi/ic_logo_wordmark_normal.webp b/mobile/android/fenix/app/src/beta/res/drawable-xhdpi/ic_logo_wordmark_normal.webp
new file mode 100644
index 0000000000..597e6d55f3
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-xhdpi/ic_logo_wordmark_normal.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-xhdpi/ic_logo_wordmark_private.webp b/mobile/android/fenix/app/src/beta/res/drawable-xhdpi/ic_logo_wordmark_private.webp
new file mode 100644
index 0000000000..7d50f610bb
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-xhdpi/ic_logo_wordmark_private.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-xxhdpi/ic_logo_wordmark_normal.webp b/mobile/android/fenix/app/src/beta/res/drawable-xxhdpi/ic_logo_wordmark_normal.webp
new file mode 100644
index 0000000000..997cb07e91
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-xxhdpi/ic_logo_wordmark_normal.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-xxhdpi/ic_logo_wordmark_private.webp b/mobile/android/fenix/app/src/beta/res/drawable-xxhdpi/ic_logo_wordmark_private.webp
new file mode 100644
index 0000000000..78acfe69d7
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-xxhdpi/ic_logo_wordmark_private.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-xxxhdpi/ic_logo_wordmark_normal.webp b/mobile/android/fenix/app/src/beta/res/drawable-xxxhdpi/ic_logo_wordmark_normal.webp
new file mode 100644
index 0000000000..220d5fb37c
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-xxxhdpi/ic_logo_wordmark_normal.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable-xxxhdpi/ic_logo_wordmark_private.webp b/mobile/android/fenix/app/src/beta/res/drawable-xxxhdpi/ic_logo_wordmark_private.webp
new file mode 100644
index 0000000000..2f45838aab
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable-xxxhdpi/ic_logo_wordmark_private.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable/animated_splash_screen.xml b/mobile/android/fenix/app/src/beta/res/drawable/animated_splash_screen.xml
new file mode 100644
index 0000000000..02b55b57a3
--- /dev/null
+++ b/mobile/android/fenix/app/src/beta/res/drawable/animated_splash_screen.xml
@@ -0,0 +1,566 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/beta/res/drawable/ic_launcher_foreground.xml b/mobile/android/fenix/app/src/beta/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..736e0f6aef
--- /dev/null
+++ b/mobile/android/fenix/app/src/beta/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_logo.webp b/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_logo.webp
new file mode 100644
index 0000000000..406889f1b6
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_logo.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_text_normal.webp b/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_text_normal.webp
new file mode 100644
index 0000000000..8e0e336857
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_text_normal.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_text_private.webp b/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_text_private.webp
new file mode 100644
index 0000000000..33a7bada6e
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/drawable/ic_wordmark_text_private.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-hdpi/ic_launcher.webp b/mobile/android/fenix/app/src/beta/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000000..705987f0bd
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/beta/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..1710c1b644
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-mdpi/ic_launcher.webp b/mobile/android/fenix/app/src/beta/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000000..eac2ba11c5
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/beta/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..17186974ad
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-xhdpi/ic_launcher.webp b/mobile/android/fenix/app/src/beta/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..7bb69c9fbf
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..66db0df3a2
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-xxhdpi/ic_launcher.webp b/mobile/android/fenix/app/src/beta/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..d63a589378
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..4dbc3878ec
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile/android/fenix/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..2a2421af1c
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..70f7527d6b
Binary files /dev/null and b/mobile/android/fenix/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/beta/res/values/colors.xml b/mobile/android/fenix/app/src/beta/res/values/colors.xml
new file mode 100644
index 0000000000..25c95838db
--- /dev/null
+++ b/mobile/android/fenix/app/src/beta/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+
+ @color/photonInk80
+
diff --git a/mobile/android/fenix/app/src/beta/res/values/static_strings.xml b/mobile/android/fenix/app/src/beta/res/values/static_strings.xml
new file mode 100644
index 0000000000..18bddf2c9e
--- /dev/null
+++ b/mobile/android/fenix/app/src/beta/res/values/static_strings.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ Firefox Beta
+
diff --git a/mobile/android/fenix/app/src/beta/res/xml/shortcuts.xml b/mobile/android/fenix/app/src/beta/res/xml/shortcuts.xml
new file mode 100644
index 0000000000..dcf89bab7b
--- /dev/null
+++ b/mobile/android/fenix/app/src/beta/res/xml/shortcuts.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/debug/AndroidManifest.xml b/mobile/android/fenix/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000000..3597f244e3
--- /dev/null
+++ b/mobile/android/fenix/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/debug/ic_launcher-web.webp b/mobile/android/fenix/app/src/debug/ic_launcher-web.webp
new file mode 100644
index 0000000000..7e09e488ad
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/ic_launcher-web.webp differ
diff --git a/mobile/android/fenix/app/src/debug/java/org/mozilla/fenix/DebugFenixApplication.kt b/mobile/android/fenix/app/src/debug/java/org/mozilla/fenix/DebugFenixApplication.kt
new file mode 100644
index 0000000000..71efc6421d
--- /dev/null
+++ b/mobile/android/fenix/app/src/debug/java/org/mozilla/fenix/DebugFenixApplication.kt
@@ -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
+
+import android.os.StrictMode
+import androidx.preference.PreferenceManager
+import leakcanary.AppWatcher
+import leakcanary.LeakCanary
+import org.mozilla.fenix.ext.application
+import org.mozilla.fenix.ext.getPreferenceKey
+
+class DebugFenixApplication : FenixApplication() {
+
+ override fun setupLeakCanary() {
+ if (!AppWatcher.isInstalled) {
+ AppWatcher.manualInstall(
+ application = application,
+ watchersToInstall = AppWatcher.appDefaultWatchers(application),
+ )
+ }
+
+ val isEnabled = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .getBoolean(getPreferenceKey(R.string.pref_key_leakcanary), BuildConfig.LEAKCANARY)
+ }
+
+ updateLeakCanaryState(isEnabled)
+ }
+
+ override fun updateLeakCanaryState(isEnabled: Boolean) {
+ LeakCanary.showLeakDisplayActivityLauncherIcon(isEnabled)
+ components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ LeakCanary.config = LeakCanary.config.copy(dumpHeap = isEnabled)
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/debug/res/drawable/animated_splash_screen.xml b/mobile/android/fenix/app/src/debug/res/drawable/animated_splash_screen.xml
new file mode 100644
index 0000000000..e2b7ead518
--- /dev/null
+++ b/mobile/android/fenix/app/src/debug/res/drawable/animated_splash_screen.xml
@@ -0,0 +1,1036 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/debug/res/drawable/ic_launcher_foreground.xml b/mobile/android/fenix/app/src/debug/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..844e479ef4
--- /dev/null
+++ b/mobile/android/fenix/app/src/debug/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-hdpi/ic_launcher.webp b/mobile/android/fenix/app/src/debug/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000000..89cb320575
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..be7dca2cfc
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-mdpi/ic_launcher.webp b/mobile/android/fenix/app/src/debug/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000000..f66844b075
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..6293cbc774
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp b/mobile/android/fenix/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..587f881a1d
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..7335929883
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp b/mobile/android/fenix/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..36dfdf3989
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..b8981becf0
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile/android/fenix/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..5a943bd097
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/android/fenix/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..4e5d20ba16
Binary files /dev/null and b/mobile/android/fenix/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/fenix/app/src/debug/res/raw/initial_experiments.json b/mobile/android/fenix/app/src/debug/res/raw/initial_experiments.json
new file mode 100644
index 0000000000..6bced9269e
--- /dev/null
+++ b/mobile/android/fenix/app/src/debug/res/raw/initial_experiments.json
@@ -0,0 +1,86 @@
+{
+ "data": [{
+ "slug": "feature-text-variables-validation-android",
+ "appId": "org.mozilla.fenix",
+ "appName": "fenix",
+ "channel": "nightly",
+ "branches": [
+ {
+ "slug": "no-mr2",
+ "ratio": 0,
+ "feature": {
+ "value": {
+ "sections-enabled": {
+ "topSites": true,
+ "recentExplorations": true,
+ "recentlySaved": false,
+ "jumpBackIn": false,
+ "pocket": false
+ }
+ },
+ "enabled": true,
+ "featureId": "homescreen"
+ }
+ },
+ {
+ "slug": "full-mr2",
+ "ratio": 100,
+ "feature": {
+ "value": {
+ "sections-enabled": {
+ "topSites": true,
+ "recentExplorations": true,
+ "recentlySaved": true,
+ "jumpBackIn": true,
+ "pocket": true
+ }
+ },
+ "enabled": true,
+ "featureId": "homescreen"
+ }
+ },
+ {
+ "slug": "distraction-free",
+ "ratio": 0,
+ "feature": {
+ "value": {
+ "sections-enabled": {
+ "topSites": true,
+ "recentExplorations": false,
+ "recentlySaved": false,
+ "jumpBackIn": false,
+ "pocket": false
+ }
+ },
+ "enabled": true,
+ "featureId": "homescreen"
+ }
+ }
+ ],
+ "outcomes": [],
+ "arguments": {},
+ "probeSets": [],
+ "startDate": null,
+ "targeting": "true",
+ "featureIds": [
+ "homescreen"
+ ],
+ "application": "org.mozilla.firefox_beta",
+ "bucketConfig": {
+ "count": 0,
+ "start": 0,
+ "total": 10000,
+ "namespace": "nimbus-validation-2",
+ "randomizationUnit": "nimbus_id"
+ },
+ "schemaVersion": "1.5.0",
+ "userFacingName": "Home screen sections test",
+ "referenceBranch": "control",
+ "proposedDuration": 14,
+ "isEnrollmentPaused": false,
+ "proposedEnrollment": 7,
+ "userFacingDescription": "Experiment to test the home screen configurations",
+ "last_modified": 1621443780172
+ }
+ ]
+}
diff --git a/mobile/android/fenix/app/src/debug/res/values/colors.xml b/mobile/android/fenix/app/src/debug/res/values/colors.xml
new file mode 100644
index 0000000000..31d64e9680
--- /dev/null
+++ b/mobile/android/fenix/app/src/debug/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+
+ @color/photonInk20
+
diff --git a/mobile/android/fenix/app/src/debug/res/xml/shortcuts.xml b/mobile/android/fenix/app/src/debug/res/xml/shortcuts.xml
new file mode 100644
index 0000000000..a0af5e9191
--- /dev/null
+++ b/mobile/android/fenix/app/src/debug/res/xml/shortcuts.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/main/AndroidManifest.xml b/mobile/android/fenix/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..dfe1b757f8
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/AndroidManifest.xml
@@ -0,0 +1,407 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/main/assets/highRiskErrorPages.js b/mobile/android/fenix/app/src/main/assets/highRiskErrorPages.js
new file mode 100644
index 0000000000..f4af7d3b0d
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/highRiskErrorPages.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/**
+ * Handles the parsing of the ErrorPages URI and then passes them to injectValues
+ */
+function parseQuery(queryString) {
+ if (queryString[0] === '?') {
+ queryString = queryString.substr(1);
+ }
+ const query = Object.fromEntries(new URLSearchParams(queryString).entries());
+ injectValues(query);
+};
+
+/**
+ * Updates the HTML elements based on the queryMap
+ */
+function injectValues(queryMap) {
+ // Go through each element and inject the values
+ document.title = queryMap.title;
+ document.getElementById('errorTitleText').innerHTML = queryMap.title;
+ document.getElementById('errorShortDesc').innerHTML = queryMap.description;
+
+ // If no image is passed in, remove the element so as not to leave an empty iframe
+ const errorImage = document.getElementById('errorImage');
+ if (!queryMap.image) {
+ errorImage.remove();
+ } else {
+ errorImage.src = "resource://android/assets/" + queryMap.image;
+ }
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+ if (window.history.length == 1) {
+ document.getElementById('backButton').style.display = 'none';
+ } else {
+ document.getElementById('backButton').addEventListener('click', () => window.history.back() );
+ }
+});
+
+parseQuery(document.documentURI);
diff --git a/mobile/android/fenix/app/src/main/assets/high_risk_error_pages.html b/mobile/android/fenix/app/src/main/assets/high_risk_error_pages.html
new file mode 100644
index 0000000000..c73012b143
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/high_risk_error_pages.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/main/assets/high_risk_error_style.css b/mobile/android/fenix/app/src/main/assets/high_risk_error_style.css
new file mode 100644
index 0000000000..054b72916a
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/high_risk_error_style.css
@@ -0,0 +1,29 @@
+/* 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/. */
+
+:root {
+ --background-color: #c50042;
+ --text-color: #ffffff;
+ --primary-button-color: #e6e6eb;
+ --primary-button-text-color: #2f2c61;
+ --header-color: #ffffff;
+}
+
+p {
+ line-height: 20px;
+ margin: var(--moz-vertical-spacing) 0;
+ color: #ffffff;
+}
+
+/* On large width devices, apply specific styles here. Often triggered by landscape mode or tablets */
+@media (min-width: 550px) {
+ /* If the device is tall as well, add some padding to make content feel a bit more centered */
+ @media (min-height: 550px) {
+ #errorPageContainer {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/assets/lowMediumErrorPages.js b/mobile/android/fenix/app/src/main/assets/lowMediumErrorPages.js
new file mode 100644
index 0000000000..760ce65868
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/lowMediumErrorPages.js
@@ -0,0 +1,146 @@
+/* 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/. */
+
+/**
+ * Handles the parsing of the ErrorPages URI and then passes them to injectValues
+ */
+function parseQuery(queryString) {
+ if (queryString[0] === '?') {
+ queryString = queryString.substr(1);
+ }
+ const query = Object.fromEntries(new URLSearchParams(queryString).entries());
+ injectValues(query);
+ updateShowSSL(query);
+ updateShowHSTS(query);
+};
+
+/**
+ * Updates the HTML elements based on the queryMap
+ */
+function injectValues(queryMap) {
+ const tryAgainButton = document.getElementById('errorTryAgain');
+ const continueHttpButton = document.getElementById("continueHttp");
+ const backFromHttpButton = document.getElementById('backFromHttp');
+
+ // Go through each element and inject the values
+ document.title = queryMap.title;
+ tryAgainButton.innerHTML = queryMap.button;
+ continueHttpButton.innerHTML = queryMap.continueHttpButton;
+ backFromHttpButton.innerHTML = queryMap.badCertGoBack;
+ document.getElementById('errorTitleText').innerHTML = queryMap.title;
+ document.getElementById('errorShortDesc').innerHTML = queryMap.description;
+ document.getElementById('advancedButton').innerHTML = queryMap.badCertAdvanced;
+ document.getElementById('badCertTechnicalInfo').innerHTML = queryMap.badCertTechInfo;
+ document.getElementById('advancedPanelBackButton').innerHTML = queryMap.badCertGoBack;
+ document.getElementById('advancedPanelAcceptButton').innerHTML = queryMap.badCertAcceptTemporary;
+
+ // If no image is passed in, remove the element so as not to leave an empty iframe
+ const errorImage = document.getElementById('errorImage');
+ if (!queryMap.image) {
+ errorImage.remove();
+ } else {
+ errorImage.src = "resource://android/assets/" + queryMap.image;
+ }
+
+ if (queryMap.showContinueHttp === "true") {
+ // On the "HTTPS-Only" error page "Try again" doesn't make sense since reloading the page
+ // will just show an error page again.
+ tryAgainButton.style.display = 'none';
+ } else {
+ continueHttpButton.style.display = 'none';
+ backFromHttpButton.style.display = 'none';
+ }
+};
+
+let advancedVisible = false;
+
+/**
+ * Used to show or hide the "accept" button based on the validity of the SSL certificate
+ */
+function updateShowSSL(queryMap) {
+ /** @type {'true' | 'false'} */
+ const showSSL = queryMap.showSSL;
+ if (typeof document.addCertException === 'undefined') {
+ document.getElementById('advancedButton').style.display='none';
+ } else {
+ if (showSSL === 'true') {
+ document.getElementById('advancedButton').style.display='block';
+ } else {
+ document.getElementById('advancedButton').style.display='none';
+ }
+ }
+};
+
+/**
+ * Used to show or hide the "accept" button based for the HSTS error page
+ */
+function updateShowHSTS(queryMap) {
+ const showHSTS = queryMap.showHSTS;
+ if (showHSTS === 'true') {
+ document.getElementById('advancedButton').style.display='block';
+ document.getElementById('advancedPanelAcceptButton').style.display='none';
+ }
+};
+
+/**
+ * Used to display information about the SSL certificate in `error_pages.html`
+ */
+function toggleAdvancedAndScroll() {
+ const advancedPanel = document.getElementById('badCertAdvancedPanel');
+ if (advancedVisible) {
+ advancedPanel.style.display='none';
+ } else {
+ advancedPanel.style.display='block';
+ }
+ advancedVisible = !advancedVisible;
+
+ const horizontalLine = document.getElementById("horizontalLine");
+ const advancedPanelAcceptButton = document.getElementById(
+ "advancedPanelAcceptButton"
+ );
+ const badCertAdvancedPanel = document.getElementById(
+ "badCertAdvancedPanel"
+ );
+
+ // We know that the button is being displayed
+ if (badCertAdvancedPanel.style.display === "block") {
+ horizontalLine.hidden = false;
+ advancedPanelAcceptButton.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "nearest",
+ });
+ } else {
+ horizontalLine.hidden = true;
+ }
+};
+
+/**
+ * Used to bypass an SSL pages in `error_pages.html`
+ */
+async function acceptAndContinue(temporary) {
+ try {
+ await document.addCertException(temporary);
+ location.reload();
+ } catch (error) {
+ console.error("Unexpected error: " + error);
+ }
+};
+
+document.addEventListener('DOMContentLoaded', function () {
+ if (window.history.length == 1) {
+ document.getElementById('advancedPanelBackButton').style.display = 'none';
+ document.getElementById('backFromHttp').style.display = 'none';
+ } else {
+ document.getElementById('advancedPanelBackButton').addEventListener('click', () => window.history.back());
+ document.getElementById('backFromHttp').addEventListener('click', () => window.history.back());
+ }
+
+ document.getElementById('errorTryAgain').addEventListener('click', () => window.location.reload());
+ document.getElementById('advancedButton').addEventListener('click', toggleAdvancedAndScroll);
+ document.getElementById('advancedPanelAcceptButton').addEventListener('click', () => acceptAndContinue(true));
+ document.getElementById('continueHttp').addEventListener('click', () => document.reloadWithHttpsOnlyException());
+});
+
+parseQuery(document.documentURI);
diff --git a/mobile/android/fenix/app/src/main/assets/low_and_medium_risk_error_pages.html b/mobile/android/fenix/app/src/main/assets/low_and_medium_risk_error_pages.html
new file mode 100644
index 0000000000..62e0c70988
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/low_and_medium_risk_error_pages.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/fenix/app/src/main/assets/low_and_medium_risk_error_style.css b/mobile/android/fenix/app/src/main/assets/low_and_medium_risk_error_style.css
new file mode 100644
index 0000000000..2b2d2792af
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/low_and_medium_risk_error_style.css
@@ -0,0 +1,35 @@
+/* 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/. */
+
+:root {
+ --background-color: #f9f9fb;
+ --text-color: #15141a;
+ --primary-button-color: #312a65;
+ --primary-button-text-color: #ffffff;
+ --secondary-button-color: #e0e0e6;
+ --secondary-button-text-color: #20123a;
+ --header-color: #312a65;
+}
+
+#badCertTechnicalInfo {
+ overflow: auto;
+ white-space: pre-line;
+}
+
+#advancedPanelButtonContainer {
+ display: flex;
+ justify-content: center;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background-color: #15141a;
+ --text-color: #fbfbfe;
+ --primary-button-color: #9059ff;
+ --primary-button-text-color: #ffffff;
+ --secondary-button-color: #e0e0e6;
+ --secondary-button-text-color: #312a65;
+ --header-color: #fbfbfe;
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/assets/searchplugins/reddit.xml b/mobile/android/fenix/app/src/main/assets/searchplugins/reddit.xml
new file mode 100644
index 0000000000..4f761ba236
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/searchplugins/reddit.xml
@@ -0,0 +1,11 @@
+
+
+
+ Reddit
+ Search Reddit
+ Reddit Search
+ data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAD9UlEQVR4AcyYA9QkSwyF69k2j59te/Bs27Zt2/batm3btu1v+55eN6p6uPec/P/MlJJCchNTCHCj2YGsuYq0+dqTxp4MJ2VmkDZLkfifh6tNfdRXY0w5weVmD1LmHtKmticLvc8kEY3RWM2huUqn+JVmR9LmDU/mSZFCiObSnJq7eIrfaLYibR7yZJIWLYZobq2htQqr/BXmIFKmuyeUSLprzcIonzanW3a9mKdxen7Kp8wdnizWhGWSxdIhH+XZTOSOxNem1DtvkcXSyf3BFvrOP3A0PHMOXLFjsO3q3eCTO+C7x+C6PWPfhHSzusqieJuxg0FYsRyG94YGv8IX98FjJ8PwXqzD1LGxRki3WBcrH1xw5bXDq1bByyl4/gL4/SVoVwMpG4rvH7d5p4ciI2xR3OVLl8DyZXDZ9sG2bx4mAF0lm3sNi9gK5UV5gH++BkO7h7ddtQuMG8I6TBoJ1+zuEiPeCBCzPLlNtHSsA3V/jG6/cmf4/F748n5dN2futBEBFCMsmgucMQk+vasYc9+z4fWpXRTlbz0YhHsPLwbVqL0uGYnm83nKO9fB/NmQ2qIYBiyU7kbZUcEmlaIPHgNvXQ3fPAI9m8O08b5rfPsaeOg4SG9ZMCOku1GKl9ckl24LH9wMnerBvFlYoRNR3w9v1dh8T+FrGdDYaWdfzcJvL/o7md0GMlvDL8/B7GnkjDnT4feXfUMyW/knpzXeuELfXQxoLAOGx3YSf+nbho0wsi+M6k/BoFgwtAcbYVAXa0yQ7kYVg9hO1T6nbGj4m+0UZugEltqIWNkwY5LtBJbaDZg0iliIlInviBaIsOl7dF/1UV+Nie8rzJ1hNcB+hWp9SwykyEb9pWAE1BYgerFo+o/1CtkfsXarfwciIC4T6B8Jtbn2HT0Art/b+ojd3KiCz6ypbrv6StrxBCyndeVOTm7UPZDNmmK91/4bGEck1ObSVwnQZds5BjJXKjGgIyWDDHOmEq5krupnlAytKrmTOWc6fduhSsopCZ44zZ1OJ0po/n+PUMiwmw+Av9+AxQuJxLKlUOMruGEfWDCHUDT5O3lC45xSyht1aUAoWlb0SyJyq8oD/n8X6v8Ctb+DX1+Ad6+Ha/eAq3eFRr8TiiHdlPwnTykTJfVioo3+iKbKCj6iyo+cCDcf6J+M8oD3bhC3iWavoth+THFP6vMqq3x8O0yfQN6YOxO+fyKY6NjLKvkXtkS1lRMoaiaGSij/vGWrxLkXtvIuLerK/Pg0tKqsXMFnkksX+w9XQXDMQGhf06/MPX1WDnny+tJi4uJu+WX1EIO7Q3p4fUhPcAzpKaaRO8k38NOsoxPd9F9qMLrYY6gttwEAUidatlp2alsAAAAASUVORK5CYII=
+
+
diff --git a/mobile/android/fenix/app/src/main/assets/searchplugins/youtube.xml b/mobile/android/fenix/app/src/main/assets/searchplugins/youtube.xml
new file mode 100644
index 0000000000..60a7897ae4
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/searchplugins/youtube.xml
@@ -0,0 +1,12 @@
+
+
+
+ YouTube
+ Search for videos on YouTube
+ youtube video
+ data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABa0lEQVR42u2YoYoCURSGBzSLk62L0TJvsEywWAxabRaFlQ2GQdY38AF8Ci0Gsy9gECybRU0GkQn++4fLbjm7K8wdmQPng6+I3rkfqHPnBIZhGIbxCAiCkL7QiL7SFu3QHu3TIX2nY5rQCf1wTtxrY/eeoftMz63RcmtG7hqhj82+0RX9pDeKJ3tz1165vVQf3XxMTxQF80Tj/zbfoFeKgnqljb8C1hQFd/3b5mv0riDgTmtSQJtCiW0pIFEUkEgBc0UBcylgkXnhKAIqlWcELKSATeaFp1PgeAT6faBUyjNgIwXsfQR8s90CcZxXwF4KOHgM+GG5BOp13wEHKeDiP8CRpsBsBoShr4CLFJB6DJA5n4HBACiXswakUsA9hwCZ3Q5oNjPdjfUH6P8K6f8R6/8b1X8j03+U0H+Y03+c1v9Ao/+RUv9Dvf6xiv7Blv7RojzcHRVouDui1TzG610P4/WuOF43DMMwjAf4ArcA0xFiL427AAAAAElFTkSuQmCC
+
+
+
diff --git a/mobile/android/fenix/app/src/main/assets/shared_error_style.css b/mobile/android/fenix/app/src/main/assets/shared_error_style.css
new file mode 100644
index 0000000000..5753b00a78
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/assets/shared_error_style.css
@@ -0,0 +1,150 @@
+/* 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/. */
+
+:root {
+ --moz-vertical-spacing: 10px;
+ --moz-background-height: 32px;
+ /* Default values just to indicate what color variables we use */
+ --background-color: #f9f9fb;
+ --text-color: #15141a;
+ --primary-button-color: #312a65;
+ --primary-button-text-color: #ffffff;
+ --secondary-button-color: #e0e0e6;
+ --secondary-button-text-color: #20123a;
+ --header-color: var(--text-color);
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+body {
+ background-size: 64px var(--moz-background-height);
+ background-repeat: repeat-x;
+ background-color: var(--background-color);
+ color: var(--text-color);
+ padding: 0 40px;
+ font-size: 14px;
+ font-family: sharp-sans, sans-serif;
+ -moz-text-size-adjust: none;
+}
+
+ul {
+ /* Shove the list indicator so that its left aligned, but use outside so that text
+ * doesn't don't wrap the text around it */
+ padding: 0 1em;
+ margin: 0;
+ list-style-type: disc;
+}
+
+#errorShortDesc,
+li:not(:last-of-type) {
+ /* Margins between the li and buttons below it won't be collapsed. Remove the bottom margin here. */
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+h1 {
+ margin: 0;
+ padding: 0;
+ margin: var(--moz-vertical-spacing) 0;
+ color: var(--header-color);
+ font-weight: bold;
+ font-size: 20px;
+ line-height: 24px;
+}
+
+p {
+ line-height: 20px;
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+button {
+ display: block;
+ height: 36px;
+ box-sizing: content-box;
+ width: 100%;
+ border: 0;
+ padding: 6px 0px;
+ font-family: inherit;
+ background-color: transparent;
+ color: var(--primary-button-text-color);
+ font-size: 14px;
+ font-weight: bold;
+ margin: 0 auto;
+ text-align: center;
+ position: relative;
+}
+
+button::after {
+ background-color: var(--primary-button-color);
+ content: '';
+ border-radius: 5px;
+ display: block;
+ position: absolute;
+ top: 6px;
+ left: 0px;
+ right: 0px;
+ bottom: 6px;
+ z-index: -1;
+}
+
+hr {
+ height: 1px;
+ border: 0;
+ background: rgba(21, 20, 26, 0.12);
+ margin: 32px 0;
+}
+
+.horizontalLine {
+ margin-left: -40px;
+ margin-right: -40px;
+}
+
+.buttonSecondary {
+ background-color: transparent;
+ color: var(--secondary-button-text-color);
+}
+
+.buttonSecondary::after {
+ background-color: var(--secondary-button-color);
+}
+
+#errorPageContainer {
+ /* If the page is greater than 550px center the content.
+ * This number should be kept in sync with the media query for tablets below */
+ max-width: 550px;
+ margin: 0 auto;
+ min-height: 100%;
+}
+
+/* On large width devices, apply specific styles here. Often triggered by landscape mode or tablets */
+@media (min-width: 550px) {
+ button,
+ .buttonSecondary {
+ margin: var(--moz-vertical-spacing) auto;
+ min-width: 400px;
+ width: auto;
+ }
+
+ /* If the device is tall as well, add some padding to make content feel a bit more centered */
+ @media (min-height: 550px) {
+ #errorPageContainer {
+ padding-top: 64px;
+ min-height: calc(100% - 64px);
+ }
+ }
+}
+
+#badCertTechnicalInfo {
+ overflow: auto;
+ white-space: pre-line;
+}
+
+#advancedPanelButtonContainer {
+ display: flex;
+ justify-content: center;
+}
diff --git a/mobile/android/fenix/app/src/main/ic_launcher-web.webp b/mobile/android/fenix/app/src/main/ic_launcher-web.webp
new file mode 100644
index 0000000000..6cf56fdd15
Binary files /dev/null and b/mobile/android/fenix/app/src/main/ic_launcher-web.webp differ
diff --git a/mobile/android/fenix/app/src/main/ic_launcher_private-web.webp b/mobile/android/fenix/app/src/main/ic_launcher_private-web.webp
new file mode 100644
index 0000000000..05aa7567f8
Binary files /dev/null and b/mobile/android/fenix/app/src/main/ic_launcher_private-web.webp differ
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt
new file mode 100644
index 0000000000..c1c00d4995
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt
@@ -0,0 +1,170 @@
+/* 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 java.lang.ref.WeakReference
+
+class AppRequestInterceptor(
+ private val context: Context,
+) : RequestInterceptor {
+
+ private var navController: WeakReference? = null
+
+ fun setNavigationController(navController: NavController) {
+ this.navController = WeakReference(navController)
+ }
+
+ override fun interceptsAppInitiatedRequests() = true
+
+ override fun onLoadRequest(
+ engineSession: EngineSession,
+ uri: String,
+ lastUri: String?,
+ hasUserGesture: Boolean,
+ isSameDomain: Boolean,
+ isRedirect: Boolean,
+ isDirectNavigation: Boolean,
+ isSubframeRequest: Boolean,
+ ): RequestInterceptor.InterceptionResponse? {
+ val services = context.components.services
+
+ return services.urlRequestInterceptor.onLoadRequest(
+ engineSession,
+ uri,
+ lastUri,
+ hasUserGesture,
+ isSameDomain,
+ isRedirect,
+ isDirectNavigation,
+ isSubframeRequest,
+ ) ?: 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)
+ }
+
+ /**
+ * 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()!!.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) +
+ "
" +
+ 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"
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt
new file mode 100644
index 0000000000..9b20c6596c
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt
@@ -0,0 +1,44 @@
+/* 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 androidx.annotation.IdRes
+
+/**
+ * Used with [HomeActivity.openToBrowser] to indicate which fragment
+ * the browser is being opened from.
+ *
+ * @property fragmentId ID of the fragment opening the browser in the navigation graph.
+ * An ID of `0` indicates a global action with no corresponding opening fragment.
+ */
+enum class BrowserDirection(@IdRes val fragmentId: Int) {
+ FromGlobal(0),
+ FromHome(R.id.homeFragment),
+ FromWallpaper(R.id.wallpaperSettingsFragment),
+ FromSearchDialog(R.id.searchDialogFragment),
+ FromSettings(R.id.settingsFragment),
+ FromBookmarks(R.id.bookmarkFragment),
+ FromHistory(R.id.historyFragment),
+ FromHistoryMetadataGroup(R.id.historyMetadataGroupFragment),
+ FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment),
+ FromAbout(R.id.aboutFragment),
+ FromTrackingProtection(R.id.trackingProtectionFragment),
+ FromHttpsOnlyMode(R.id.httpsOnlyFragment),
+ FromTrackingProtectionDialog(R.id.trackingProtectionPanelDialogFragment),
+ FromSavedLoginsFragment(R.id.savedLoginsFragment),
+ FromAddNewDeviceFragment(R.id.addNewDeviceFragment),
+ FromSearchEngineFragment(R.id.searchEngineFragment),
+ FromSaveSearchEngineFragment(R.id.saveSearchEngineFragment),
+ FromAddonDetailsFragment(R.id.addonDetailsFragment),
+ FromStudiesFragment(R.id.studiesFragment),
+ FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
+ FromLoginDetailFragment(R.id.loginDetailFragment),
+ FromTabsTray(R.id.tabsTrayFragment),
+ FromRecentlyClosed(R.id.recentlyClosedFragment),
+ FromReviewQualityCheck(R.id.reviewQualityCheckFragment),
+ FromAddonsManagementFragment(R.id.addonsManagementFragment),
+ FromTranslationsDialogFragment(R.id.translationsDialogFragment),
+ FromMenuDialogFragment(R.id.menuDialogFragment),
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/Config.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/Config.kt
new file mode 100644
index 0000000000..534cf2d804
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/Config.kt
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix
+
+enum class ReleaseChannel {
+ Debug,
+ Nightly,
+ Beta,
+ Release,
+ ;
+
+ val isReleased: Boolean
+ get() = when (this) {
+ Debug -> false
+ else -> true
+ }
+
+ /**
+ * True if this is a debug release channel, false otherwise.
+ *
+ * This constant should often be used instead of [BuildConfig.DEBUG], which indicates
+ * if the `debuggable` flag is set which can be true even on released channel builds
+ * (e.g. performance).
+ */
+ val isDebug: Boolean
+ get() = !this.isReleased
+
+ val isReleaseOrBeta: Boolean
+ get() = this == Release || this == Beta
+
+ val isRelease: Boolean
+ get() = when (this) {
+ Release -> true
+ else -> false
+ }
+
+ val isBeta: Boolean
+ get() = this == Beta
+
+ val isNightlyOrDebug: Boolean
+ get() = this == Debug || this == Nightly
+
+ /**
+ * Is this a "Mozilla Online" build of Fenix? "Mozilla Online" is the Chinese branch of Mozilla
+ * and this flag will be `true` for builds shipping to Chinese app stores.
+ */
+ val isMozillaOnline: Boolean
+ get() = BuildConfig.MOZILLA_ONLINE
+}
+
+object Config {
+ val channel = when (BuildConfig.BUILD_TYPE) {
+ "debug" -> ReleaseChannel.Debug
+ "nightly", "benchmark" -> ReleaseChannel.Nightly
+ "beta" -> ReleaseChannel.Beta
+ "release" -> ReleaseChannel.Release
+ else -> {
+ throw IllegalStateException("Unknown build type: ${BuildConfig.BUILD_TYPE}")
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt
new file mode 100644
index 0000000000..aa8a5250a7
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt
@@ -0,0 +1,90 @@
+/* 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 mozilla.components.support.locale.LocaleManager
+import mozilla.components.support.locale.LocaleManager.getSystemDefault
+
+/**
+ * A single source for setting feature flags that are mostly based on build type.
+ */
+object FeatureFlags {
+
+ /**
+ * Enables custom extension collection feature,
+ * This feature does not only depend on this flag. It requires the AMO collection override to
+ * be enabled which is behind the Secret Settings.
+ * */
+ val customExtensionCollectionFeature = Config.channel.isNightlyOrDebug || Config.channel.isBeta
+
+ /**
+ * Pull-to-refresh allows you to pull the web content down far enough to have the page to
+ * reload.
+ */
+ const val pullToRefreshEnabled = true
+
+ /**
+ * Enables the Sync Addresses feature.
+ */
+ const val syncAddressesFeature = false
+
+ /**
+ * Show Pocket recommended stories on home.
+ */
+ fun isPocketRecommendationsFeatureEnabled(context: Context): Boolean {
+ val langTag = LocaleManager.getCurrentLocale(context)
+ ?.toLanguageTag() ?: getSystemDefault().toLanguageTag()
+ return listOf("en-US", "en-CA").contains(langTag)
+ }
+
+ /**
+ * Show Pocket sponsored stories in between Pocket recommended stories on home.
+ */
+ fun isPocketSponsoredStoriesFeatureEnabled(context: Context): Boolean {
+ return isPocketRecommendationsFeatureEnabled(context)
+ }
+
+ /**
+ * Enables compose on the tabs tray items.
+ */
+ val composeTabsTray = Config.channel.isNightlyOrDebug || Config.channel.isBeta
+
+ /**
+ * Enables compose on the top sites.
+ */
+ const val composeTopSites = false
+
+ /**
+ * Enables new search settings UI with two extra fragments, for managing the default engine
+ * and managing search shortcuts in the quick search menu.
+ */
+ const val unifiedSearchSettings = true
+
+ /**
+ * Allows users to enable Firefox Suggest.
+ */
+ const val fxSuggest = true
+
+ /**
+ * Allows users to enable SuggestStrongPassword feature.
+ */
+ const val suggestStrongPassword = true
+
+ /**
+ * Enable Meta attribution.
+ */
+ const val metaAttributionEnabled = true
+
+ /**
+ * Enable Toolbar Redesign components and behaviors ready for Nightly.
+ */
+ val completeToolbarRedesignEnabled = Config.channel.isNightlyOrDebug
+
+ /**
+ * Enables the menu redesign.
+ */
+ const val menuRedesignEnabled = false
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt
new file mode 100644
index 0000000000..7c32c02f60
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt
@@ -0,0 +1,1049 @@
+/* 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.annotation.SuppressLint
+import android.app.ActivityManager
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.StrictMode
+import android.os.SystemClock
+import android.util.Log.INFO
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.getSystemService
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.work.Configuration.Builder
+import androidx.work.Configuration.Provider
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import mozilla.appservices.Megazord
+import mozilla.appservices.autofill.AutofillApiException
+import mozilla.components.browser.state.action.SystemAction
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.searchEngines
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.concept.engine.webextension.WebExtension
+import mozilla.components.concept.engine.webextension.isUnsupported
+import mozilla.components.concept.push.PushProcessor
+import mozilla.components.concept.storage.FrecencyThresholdOption
+import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
+import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
+import mozilla.components.feature.autofill.AutofillUseCases
+import mozilla.components.feature.fxsuggest.GlobalFxSuggestDependencyProvider
+import mozilla.components.feature.search.ext.buildSearchUrl
+import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine
+import mozilla.components.feature.top.sites.TopSitesFrecencyConfig
+import mozilla.components.feature.top.sites.TopSitesProviderConfig
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.service.fxa.manager.SyncEnginesStorage
+import mozilla.components.service.glean.Glean
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader
+import mozilla.components.service.sync.logins.LoginsApiException
+import mozilla.components.support.base.ext.areNotificationsEnabledSafe
+import mozilla.components.support.base.ext.isNotificationChannelEnabled
+import mozilla.components.support.base.facts.register
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
+import mozilla.components.support.ktx.android.content.isMainProcess
+import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
+import mozilla.components.support.locale.LocaleAwareApplication
+import mozilla.components.support.rusterrors.initializeRustErrors
+import mozilla.components.support.rusthttp.RustHttpConfig
+import mozilla.components.support.rustlog.RustLog
+import mozilla.components.support.utils.BrowsersCache
+import mozilla.components.support.utils.logElapsedTime
+import mozilla.components.support.webextensions.WebExtensionSupport
+import org.mozilla.fenix.GleanMetrics.Addons
+import org.mozilla.fenix.GleanMetrics.Addresses
+import org.mozilla.fenix.GleanMetrics.AndroidAutofill
+import org.mozilla.fenix.GleanMetrics.CreditCards
+import org.mozilla.fenix.GleanMetrics.CustomizeHome
+import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
+import org.mozilla.fenix.GleanMetrics.GleanBuildInfo
+import org.mozilla.fenix.GleanMetrics.Logins
+import org.mozilla.fenix.GleanMetrics.Metrics
+import org.mozilla.fenix.GleanMetrics.PerfStartup
+import org.mozilla.fenix.GleanMetrics.Preferences
+import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
+import org.mozilla.fenix.GleanMetrics.ShoppingSettings
+import org.mozilla.fenix.GleanMetrics.TabStrip
+import org.mozilla.fenix.GleanMetrics.TopSites
+import org.mozilla.fenix.components.Components
+import org.mozilla.fenix.components.Core
+import org.mozilla.fenix.components.appstate.AppAction
+import org.mozilla.fenix.components.metrics.MetricServiceType
+import org.mozilla.fenix.components.metrics.MozillaProductDetector
+import org.mozilla.fenix.experiments.maybeFetchExperiments
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.containsQueryParameters
+import org.mozilla.fenix.ext.getCustomGleanServerUrlIfAvailable
+import org.mozilla.fenix.ext.isCustomEngine
+import org.mozilla.fenix.ext.isKnownSearchDomain
+import org.mozilla.fenix.ext.setCustomEndpointIfAvailable
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.lifecycle.StoreLifecycleObserver
+import org.mozilla.fenix.nimbus.FxNimbus
+import org.mozilla.fenix.onboarding.MARKETING_CHANNEL_ID
+import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
+import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
+import org.mozilla.fenix.perf.StartupTimeline
+import org.mozilla.fenix.perf.StorageStatsMetrics
+import org.mozilla.fenix.perf.runBlockingIncrement
+import org.mozilla.fenix.push.PushFxaIntegration
+import org.mozilla.fenix.push.WebPushEngineIntegration
+import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
+import org.mozilla.fenix.session.VisibilityLifecycleCallback
+import org.mozilla.fenix.utils.Settings
+import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
+import org.mozilla.fenix.wallpapers.Wallpaper
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToLong
+
+private const val RAM_THRESHOLD_MEGABYTES = 1024
+private const val BYTES_TO_MEGABYTES_CONVERSION = 1024.0 * 1024.0
+
+/**
+ *The main application class for Fenix. Records data to measure initialization performance.
+ * Installs [CrashReporter], initializes [Glean] in fenix builds and setup Megazord in the main process.
+ */
+@Suppress("Registered", "TooManyFunctions", "LargeClass")
+open class FenixApplication : LocaleAwareApplication(), Provider {
+ init {
+ recordOnInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing of this measurement is critical.
+ }
+
+ private val logger = Logger("FenixApplication")
+
+ internal val isDeviceRamAboveThreshold by lazy {
+ isDeviceRamAboveThreshold()
+ }
+
+ open val components by lazy { Components(this) }
+
+ var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
+ private set
+
+ override fun onCreate() {
+ super.onCreate()
+
+ if (shouldShowPrivacyNotice()) {
+ // For Mozilla Online build: Delay initialization on first run until privacy notice
+ // is accepted by the user.
+ return
+ }
+
+ initialize()
+ }
+
+ /**
+ * Initializes Fenix and all required subsystems such as Nimbus, Glean and Gecko.
+ */
+ fun initialize() {
+ // We measure ourselves to avoid a call into Glean before its loaded.
+ val start = SystemClock.elapsedRealtimeNanos()
+
+ setupInAllProcesses()
+
+ if (!isMainProcess()) {
+ // If this is not the main process then do not continue with the initialization here. Everything that
+ // follows only needs to be done in our app's main process and should not be done in other processes like
+ // a GeckoView child process or the crash handling process. Most importantly we never want to end up in a
+ // situation where we create a GeckoRuntime from the Gecko child process.
+ return
+ }
+
+ // DO NOT ADD ANYTHING ABOVE HERE.
+ setupInMainProcessOnly()
+ // DO NOT ADD ANYTHING UNDER HERE.
+
+ // DO NOT MOVE ANYTHING BELOW THIS elapsedRealtimeNanos CALL.
+ val stop = SystemClock.elapsedRealtimeNanos()
+ val durationMillis = TimeUnit.NANOSECONDS.toMillis(stop - start)
+
+ // We avoid blocking the main thread on startup by calling into Glean on the background thread.
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(Dispatchers.IO) {
+ PerfStartup.applicationOnCreate.accumulateSamples(listOf(durationMillis))
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ @VisibleForTesting
+ protected open fun initializeGlean() {
+ val telemetryEnabled = settings().isTelemetryEnabled
+
+ logger.debug("Initializing Glean (uploadEnabled=$telemetryEnabled})")
+
+ // for performance reasons, this is only available in Nightly or Debug builds
+ val customEndpoint = if (Config.channel.isNightlyOrDebug) {
+ // for testing, if custom glean server url is set in the secret menu, use it to initialize Glean
+ getCustomGleanServerUrlIfAvailable(this)
+ } else {
+ null
+ }
+
+ val configuration = Configuration(
+ channel = BuildConfig.BUILD_TYPE,
+ httpClient = ConceptFetchHttpUploader(
+ lazy(LazyThreadSafetyMode.NONE) { components.core.client },
+ ),
+ enableEventTimestamps = FxNimbus.features.glean.value().enableEventTimestamps,
+ )
+
+ // Set the metric configuration from Nimbus.
+ Glean.setMetricsEnabledConfig(FxNimbus.features.glean.value().metricsEnabled)
+
+ Glean.initialize(
+ applicationContext = this,
+ configuration = configuration.setCustomEndpointIfAvailable(customEndpoint),
+ uploadEnabled = telemetryEnabled,
+ buildInfo = GleanBuildInfo.buildInfo,
+ )
+
+ // We avoid blocking the main thread on startup by setting startup metrics on the background thread.
+ val store = components.core.store
+ GlobalScope.launch(Dispatchers.IO) {
+ setStartupMetrics(store, settings())
+ }
+ }
+
+ @VisibleForTesting
+ protected open fun setupInAllProcesses() {
+ setupCrashReporting()
+
+ // We want the log messages of all builds to go to Android logcat
+ Log.addSink(FenixLogSink(logsDebug = Config.channel.isDebug, AndroidLogSink()))
+ }
+
+ @VisibleForTesting
+ protected open fun setupInMainProcessOnly() {
+ // ⚠️ DO NOT ADD ANYTHING ABOVE THIS LINE.
+ // Especially references to the engine/BrowserStore which can alter the app initialization.
+ // See: https://github.com/mozilla-mobile/fenix/issues/26320
+ //
+ // We can initialize Nimbus before Glean because Glean will queue messages
+ // before it's initialized.
+ initializeNimbus()
+
+ ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
+
+ run {
+ // Make sure the engine is initialized and ready to use.
+ components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ components.core.engine.warmUp()
+ }
+
+ // We need to always initialize Glean and do it early here.
+ initializeGlean()
+
+ // Attention: Do not invoke any code from a-s in this scope.
+ val megazordSetup = finishSetupMegazord()
+
+ setDayNightTheme()
+ components.strictMode.enableStrictMode(true)
+ warmBrowsersCache()
+
+ initializeWebExtensionSupport()
+
+ // Make sure to call this function before registering a storage worker
+ // (e.g. components.core.historyStorage.registerStorageMaintenanceWorker())
+ // as the storage maintenance worker needs a places storage globally when
+ // it is needed while the app is not running and WorkManager wakes up the app
+ // for the periodic task.
+ GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
+
+ restoreBrowserState()
+ restoreDownloads()
+ restoreMessaging()
+
+ // Just to make sure it is impossible for any application-services pieces
+ // to invoke parts of itself that require complete megazord initialization
+ // before that process completes, we wait here, if necessary.
+ if (!megazordSetup.isCompleted) {
+ runBlockingIncrement { megazordSetup.await() }
+ }
+ }
+
+ setupLeakCanary()
+ startMetricsIfEnabled()
+ setupPush()
+
+ GlobalFxSuggestDependencyProvider.initialize(components.fxSuggest.storage)
+
+ visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
+ registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
+ registerActivityLifecycleCallbacks(MarkersActivityLifecycleCallbacks(components.core.engine))
+
+ components.appStartReasonProvider.registerInAppOnCreate(this)
+ components.startupActivityLog.registerInAppOnCreate(this)
+ initVisualCompletenessQueueAndQueueTasks()
+
+ ProcessLifecycleOwner.get().lifecycle.addObservers(
+ StoreLifecycleObserver(
+ appStore = components.appStore,
+ browserStore = components.core.store,
+ ),
+ )
+
+ components.analytics.metricsStorage.tryRegisterAsUsageRecorder(this)
+
+ downloadWallpapers()
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
+ val store = components.core.store
+ val sessionStorage = components.core.sessionStorage
+
+ components.useCases.tabsUseCases.restore(sessionStorage, settings().getTabTimeout())
+
+ // Now that we have restored our previous state (if there's one) let's setup auto saving the state while
+ // the app is used.
+ sessionStorage.autoSave(store)
+ .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
+ .whenGoingToBackground()
+ .whenSessionsChange()
+ }
+
+ private fun restoreDownloads() {
+ components.useCases.downloadUseCases.restoreDownloads()
+ }
+
+ private fun initVisualCompletenessQueueAndQueueTasks() {
+ val queue = components.performance.visualCompletenessQueue.queue
+
+ fun initQueue() {
+ registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(queue))
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ fun queueInitStorageAndServices() {
+ components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
+ GlobalScope.launch(Dispatchers.IO) {
+ logger.info("Running post-visual completeness tasks...")
+ logElapsedTime(logger, "Storage initialization") {
+ components.core.historyStorage.warmUp()
+ components.core.bookmarksStorage.warmUp()
+ components.core.passwordsStorage.warmUp()
+ components.core.autofillStorage.warmUp()
+
+ // Populate the top site cache to improve initial load experience
+ // of the home fragment when the app is launched to a tab. The actual
+ // database call is not expensive. However, the additional context
+ // switches delay rendering top sites when the cache is empty, which
+ // we can prevent with this.
+ components.core.topSitesStorage.getTopSites(
+ totalSites = components.settings.topSitesMaxLimit,
+ frecencyConfig = TopSitesFrecencyConfig(
+ FrecencyThresholdOption.SKIP_ONE_TIME_PAGES,
+ ) {
+ !Uri.parse(it.url)
+ .containsQueryParameters(components.settings.frecencyFilterQuery)
+ },
+ providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = components.settings.showContileFeature,
+ maxThreshold = TOP_SITES_PROVIDER_MAX_THRESHOLD,
+ ),
+ )
+
+ // This service uses `historyStorage`, and so we can only touch it when we know
+ // it's safe to touch `historyStorage. By 'safe', we mainly mean that underlying
+ // places library will be able to load, which requires first running Megazord.init().
+ // The visual completeness tasks are scheduled after the Megazord.init() call.
+ components.core.historyMetadataService.cleanup(
+ System.currentTimeMillis() - Core.HISTORY_METADATA_MAX_AGE_IN_MS,
+ )
+
+ // If Firefox Suggest is enabled, register a worker to periodically ingest
+ // new search suggestions. The worker requires us to have called
+ // `GlobalFxSuggestDependencyProvider.initialize`, which we did before
+ // scheduling these tasks. When disabled we stop the periodic work.
+ if (settings().enableFxSuggest) {
+ components.fxSuggest.ingestionScheduler.startPeriodicIngestion()
+ } else {
+ components.fxSuggest.ingestionScheduler.stopPeriodicIngestion()
+ }
+ }
+ components.core.fileUploadsDirCleaner.cleanUploadsDirectory()
+ }
+ // Account manager initialization needs to happen on the main thread.
+ GlobalScope.launch(Dispatchers.Main) {
+ logElapsedTime(logger, "Kicking-off account manager") {
+ components.backgroundServices.accountManager
+ }
+ }
+ }
+ }
+
+ fun queueMetrics() {
+ if (SDK_INT >= Build.VERSION_CODES.O) { // required by StorageStatsMetrics.
+ queue.runIfReadyOrQueue {
+ // Because it may be slow to capture the storage stats, it might be preferred to
+ // create a WorkManager task for this metric, however, I ran out of
+ // implementation time and WorkManager is harder to test.
+ StorageStatsMetrics.report(this.applicationContext)
+ }
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ fun queueReviewPrompt() {
+ GlobalScope.launch(Dispatchers.IO) {
+ components.reviewPromptController.trackApplicationLaunch()
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ fun queueRestoreLocale() {
+ components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue {
+ GlobalScope.launch(Dispatchers.IO) {
+ components.useCases.localeUseCases.restore()
+ }
+ }
+ }
+
+ fun queueStorageMaintenance() {
+ queue.runIfReadyOrQueue {
+ // Make sure GlobalPlacesDependencyProvider.initialize(components.core.historyStorage)
+ // is called before this call. When app is not running and WorkManager wakes up
+ // the app for the periodic task, it will require a globally provided places storage
+ // to run the maintenance on.
+ components.core.historyStorage.registerStorageMaintenanceWorker()
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ fun queueNimbusFetchInForeground() {
+ queue.runIfReadyOrQueue {
+ GlobalScope.launch(Dispatchers.IO) {
+ components.nimbus.sdk.maybeFetchExperiments(
+ context = this@FenixApplication,
+ )
+ }
+ }
+ }
+
+ initQueue()
+
+ // We init these items in the visual completeness queue to avoid them initing in the critical
+ // startup path, before the UI finishes drawing (i.e. visual completeness).
+ queueInitStorageAndServices()
+ queueMetrics()
+ queueReviewPrompt()
+ queueRestoreLocale()
+ queueStorageMaintenance()
+ queueNimbusFetchInForeground()
+ }
+
+ private fun startMetricsIfEnabled() {
+ if (settings().isTelemetryEnabled) {
+ components.analytics.metrics.start(MetricServiceType.Data)
+ components.analytics.crashFactCollector.start()
+ }
+
+ if (settings().isMarketingTelemetryEnabled) {
+ components.analytics.metrics.start(MetricServiceType.Marketing)
+ }
+ }
+
+ protected open fun setupLeakCanary() {
+ // no-op, LeakCanary is disabled by default
+ }
+
+ open fun updateLeakCanaryState(isEnabled: Boolean) {
+ // no-op, LeakCanary is disabled by default
+ }
+
+ private fun setupPush() {
+ // Sets the PushFeature as the singleton instance for push messages to go to.
+ // We need the push feature setup here to deliver messages in the case where the service
+ // starts up the app first.
+ components.push.feature?.let {
+ logger.info("AutoPushFeature is configured, initializing it...")
+
+ // Install the AutoPush singleton to receive messages.
+ PushProcessor.install(it)
+
+ WebPushEngineIntegration(components.core.engine, it).start()
+
+ // Perform a one-time initialization of the account manager if a message is received.
+ PushFxaIntegration(it, lazy { components.backgroundServices.accountManager }).launch()
+
+ // Initialize the service. This could potentially be done in a coroutine in the future.
+ it.initialize()
+ }
+ }
+
+ private fun setupCrashReporting() {
+ components
+ .analytics
+ .crashReporter
+ .install(this)
+ }
+
+ protected open fun initializeNimbus() {
+ beginSetupMegazord()
+
+ // This lazily constructs the Nimbus object…
+ val nimbus = components.nimbus.sdk
+ // … which we then can populate the feature configuration.
+ FxNimbus.initialize { nimbus }
+ }
+
+ /**
+ * Initiate Megazord sequence! Megazord Battle Mode!
+ *
+ * The application-services combined libraries are known as the "megazord". We use the default `full`
+ * megazord - it contains everything that fenix needs, and (currently) nothing more.
+ *
+ * Documentation on what megazords are, and why they're needed:
+ * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
+ * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
+ *
+ * This is the initialization of the megazord without setting up networking, i.e. needing the
+ * engine for networking. This should do the minimum work necessary as it is done on the main
+ * thread, early in the app startup sequence.
+ */
+ private fun beginSetupMegazord() {
+ // Note: Megazord.init() must be called as soon as possible ...
+ Megazord.init()
+
+ initializeRustErrors(components.analytics.crashReporter)
+ // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
+
+ RustLog.enable()
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ private fun finishSetupMegazord(): Deferred {
+ return GlobalScope.async(Dispatchers.IO) {
+ if (Config.channel.isDebug) {
+ RustHttpConfig.allowEmulatorLoopback()
+ }
+ RustHttpConfig.setClient(lazy { components.core.client })
+
+ // Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch
+ // experiments recipes from the server.
+ }
+ }
+
+ @VisibleForTesting
+ internal fun restoreMessaging() {
+ if (settings().isExperimentationEnabled) {
+ components.appStore.dispatch(AppAction.MessagingAction.Restore)
+ }
+ }
+
+ override fun onTrimMemory(level: Int) {
+ super.onTrimMemory(level)
+
+ // Additional logging and breadcrumb to debug memory issues:
+ // https://github.com/mozilla-mobile/fenix/issues/12731
+
+ logger.info("onTrimMemory(), level=$level, main=${isMainProcess()}")
+
+ components.analytics.crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ category = "Memory",
+ message = "onTrimMemory()",
+ data = mapOf(
+ "level" to level.toString(),
+ "main" to isMainProcess().toString(),
+ ),
+ level = Breadcrumb.Level.INFO,
+ ),
+ )
+
+ runOnlyInMainProcess {
+ components.core.icons.onTrimMemory(level)
+ components.core.store.dispatch(SystemAction.LowMemoryAction(level))
+ }
+ }
+
+ @SuppressLint("WrongConstant")
+ // Suppressing erroneous lint warning about using MODE_NIGHT_AUTO_BATTERY, a likely library bug
+ private fun setDayNightTheme() {
+ val settings = this.settings()
+ when {
+ settings.shouldUseLightTheme -> {
+ AppCompatDelegate.setDefaultNightMode(
+ AppCompatDelegate.MODE_NIGHT_NO,
+ )
+ }
+ settings.shouldUseDarkTheme -> {
+ AppCompatDelegate.setDefaultNightMode(
+ AppCompatDelegate.MODE_NIGHT_YES,
+ )
+ }
+ SDK_INT < Build.VERSION_CODES.P && settings.shouldUseAutoBatteryTheme -> {
+ AppCompatDelegate.setDefaultNightMode(
+ AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY,
+ )
+ }
+ SDK_INT >= Build.VERSION_CODES.P && settings.shouldFollowDeviceTheme -> {
+ AppCompatDelegate.setDefaultNightMode(
+ AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ )
+ }
+ // First run of app no default set, set the default to Follow System for 28+ and Normal Mode otherwise
+ else -> {
+ if (SDK_INT >= Build.VERSION_CODES.P) {
+ AppCompatDelegate.setDefaultNightMode(
+ AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ )
+ settings.shouldFollowDeviceTheme = true
+ } else {
+ AppCompatDelegate.setDefaultNightMode(
+ AppCompatDelegate.MODE_NIGHT_NO,
+ )
+ settings.shouldUseLightTheme = true
+ }
+ }
+ }
+ }
+
+ /**
+ * Migrate the topic specific engine to the first general or custom search engine available.
+ */
+ private fun migrateTopicSpecificSearchEngines() {
+ components.core.store.state.search.selectedOrDefaultSearchEngine.let { currentSearchEngine ->
+ if (currentSearchEngine?.isGeneral == false) {
+ components.core.store.state.search.searchEngines.firstOrNull { nextSearchEngine ->
+ nextSearchEngine.isGeneral
+ }?.let {
+ components.useCases.searchUseCases.selectSearchEngine(it)
+ }
+ }
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
+ private fun warmBrowsersCache() {
+ // We avoid blocking the main thread for BrowsersCache on startup by loading it on
+ // background thread.
+ GlobalScope.launch(Dispatchers.Default) {
+ BrowsersCache.all(this@FenixApplication)
+ }
+ }
+
+ private fun initializeWebExtensionSupport() {
+ try {
+ GlobalAddonDependencyProvider.initialize(
+ components.addonManager,
+ components.addonUpdater,
+ onCrash = { exception ->
+ components.analytics.crashReporter.submitCaughtException(exception)
+ },
+ )
+ WebExtensionSupport.initialize(
+ components.core.engine,
+ components.core.store,
+ onNewTabOverride = { _, engineSession, url ->
+ val shouldCreatePrivateSession =
+ components.core.store.state.selectedTab?.content?.private
+ ?: components.settings.openLinksInAPrivateTab
+
+ components.useCases.tabsUseCases.addTab(
+ url = url,
+ selectTab = true,
+ engineSession = engineSession,
+ private = shouldCreatePrivateSession,
+ )
+ },
+ onCloseTabOverride = { _, sessionId ->
+ components.useCases.tabsUseCases.removeTab(sessionId)
+ },
+ onSelectTabOverride = { _, sessionId ->
+ components.useCases.tabsUseCases.selectTab(sessionId)
+ },
+ onExtensionsLoaded = { extensions ->
+ components.addonUpdater.registerForFutureUpdates(extensions)
+ subscribeForNewAddonsIfNeeded(components.supportedAddonsChecker, extensions)
+ },
+ onUpdatePermissionRequest = components.addonUpdater::onUpdatePermissionRequest,
+ )
+ } catch (e: UnsupportedOperationException) {
+ logger.error("Failed to initialize web extension support", e)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun subscribeForNewAddonsIfNeeded(
+ checker: DefaultSupportedAddonsChecker,
+ installedExtensions: List,
+ ) {
+ val hasUnsupportedAddons = installedExtensions.any { it.isUnsupported() }
+ if (hasUnsupportedAddons) {
+ checker.registerForChecks()
+ } else {
+ // As checks are a persistent subscriptions, we have to make sure
+ // we remove any previous subscriptions.
+ checker.unregisterForChecks()
+ }
+ }
+
+ /**
+ * This function is called right after Glean is initialized. Part of this function depends on
+ * shared preferences to be updated so the correct value is sent with the metrics ping.
+ *
+ * The reason we're using shared preferences to track these values is due to the limitations of
+ * the current metrics ping design. The values set here will be sent in every metrics ping even
+ * if these values have not changed since the last startup.
+ */
+ @Suppress("ComplexMethod", "LongMethod")
+ @VisibleForTesting
+ internal fun setStartupMetrics(
+ browserStore: BrowserStore,
+ settings: Settings,
+ browsersCache: BrowsersCache = BrowsersCache,
+ mozillaProductDetector: MozillaProductDetector = MozillaProductDetector,
+ ) {
+ setPreferenceMetrics(settings)
+ with(Metrics) {
+ // Set this early to guarantee it's in every ping from here on.
+ distributionId.set(
+ when (Config.channel.isMozillaOnline) {
+ true -> "MozillaOnline"
+ false -> "Mozilla"
+ },
+ )
+
+ defaultBrowser.set(browsersCache.all(applicationContext).isDefaultBrowser)
+ mozillaProductDetector.getMozillaBrowserDefault(applicationContext)?.also {
+ defaultMozBrowser.set(it)
+ }
+
+ if (settings.contileContextId.isEmpty()) {
+ settings.contileContextId = TopSites.contextId.generateAndSet().toString()
+ } else {
+ TopSites.contextId.set(UUID.fromString(settings.contileContextId))
+ }
+
+ mozillaProducts.set(
+ mozillaProductDetector.getInstalledMozillaProducts(
+ applicationContext,
+ ),
+ )
+
+ adjustCampaign.set(settings.adjustCampaignId)
+ adjustAdGroup.set(settings.adjustAdGroup)
+ adjustCreative.set(settings.adjustCreative)
+ adjustNetwork.set(settings.adjustNetwork)
+
+ settings.migrateSearchWidgetInstalledPrefIfNeeded()
+ searchWidgetInstalled.set(settings.searchWidgetInstalled)
+
+ val openTabsCount = settings.openTabsCount
+ hasOpenTabs.set(openTabsCount > 0)
+ if (openTabsCount > 0) {
+ tabsOpenCount.add(openTabsCount)
+ }
+
+ val openPrivateTabsCount = settings.openPrivateTabsCount
+ if (openPrivateTabsCount > 0) {
+ privateTabsOpenCount.add(openPrivateTabsCount)
+ }
+
+ val topSitesSize = settings.topSitesSize
+ hasTopSites.set(topSitesSize > 0)
+ if (topSitesSize > 0) {
+ topSitesCount.add(topSitesSize)
+ }
+
+ val installedAddonSize = settings.installedAddonsCount
+ Addons.hasInstalledAddons.set(installedAddonSize > 0)
+ if (installedAddonSize > 0) {
+ Addons.installedAddons.set(settings.installedAddonsList.split(','))
+ }
+
+ val enabledAddonSize = settings.enabledAddonsCount
+ Addons.hasEnabledAddons.set(enabledAddonSize > 0)
+ if (enabledAddonSize > 0) {
+ Addons.enabledAddons.set(settings.enabledAddonsList.split(','))
+ }
+
+ val desktopBookmarksSize = settings.desktopBookmarksSize
+ hasDesktopBookmarks.set(desktopBookmarksSize > 0)
+ if (desktopBookmarksSize > 0) {
+ desktopBookmarksCount.add(desktopBookmarksSize)
+ }
+
+ val mobileBookmarksSize = settings.mobileBookmarksSize
+ hasMobileBookmarks.set(mobileBookmarksSize > 0)
+ if (mobileBookmarksSize > 0) {
+ mobileBookmarksCount.add(mobileBookmarksSize)
+ }
+
+ tabViewSetting.set(settings.getTabViewPingString())
+ closeTabSetting.set(settings.getTabTimeoutPingString())
+
+ val installSourcePackage = if (SDK_INT >= Build.VERSION_CODES.R) {
+ packageManager.getInstallSourceInfo(packageName).installingPackageName
+ } else {
+ @Suppress("DEPRECATION")
+ packageManager.getInstallerPackageName(packageName)
+ }
+ installSource.set(installSourcePackage.orEmpty())
+
+ val isDefaultTheCurrentWallpaper =
+ Wallpaper.nameIsDefault(settings.currentWallpaperName)
+
+ defaultWallpaper.set(isDefaultTheCurrentWallpaper)
+
+ val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
+ notificationsAllowed.set(notificationManagerCompat.areNotificationsEnabledSafe())
+ marketingNotificationAllowed.set(
+ notificationManagerCompat.isNotificationChannelEnabled(MARKETING_CHANNEL_ID),
+ )
+
+ ramMoreThanThreshold.set(isDeviceRamAboveThreshold)
+ deviceTotalRam.set(getDeviceTotalRAM())
+ }
+
+ with(AndroidAutofill) {
+ val autofillUseCases = AutofillUseCases()
+ supported.set(autofillUseCases.isSupported(applicationContext))
+ enabled.set(autofillUseCases.isEnabled(applicationContext))
+ }
+
+ browserStore.waitForSelectedOrDefaultSearchEngine { searchEngine ->
+ searchEngine?.let {
+ val sendSearchUrl =
+ !searchEngine.isCustomEngine() || searchEngine.isKnownSearchDomain()
+ if (sendSearchUrl) {
+ SearchDefaultEngine.apply {
+ code.set(searchEngine.id)
+ name.set(searchEngine.name)
+ searchUrl.set(searchEngine.buildSearchUrl(""))
+ }
+ } else {
+ SearchDefaultEngine.apply {
+ code.set(searchEngine.id)
+ name.set("custom")
+ }
+ }
+
+ migrateTopicSpecificSearchEngines()
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(IO) {
+ try {
+ val autoFillStorage = applicationContext.components.core.autofillStorage
+ Addresses.savedAll.set(autoFillStorage.getAllAddresses().size.toLong())
+ CreditCards.savedAll.set(autoFillStorage.getAllCreditCards().size.toLong())
+ } catch (e: AutofillApiException) {
+ logger.error("Failed to fetch autofill data", e)
+ }
+
+ try {
+ val passwordsStorage = applicationContext.components.core.passwordsStorage
+ Logins.savedAll.set(passwordsStorage.list().size.toLong())
+ } catch (e: LoginsApiException) {
+ logger.error("Failed to fetch list of logins", e)
+ }
+ }
+
+ with(ShoppingSettings) {
+ componentOptedOut.set(!settings.isReviewQualityCheckEnabled)
+ nimbusDisabledShopping.set(!FxNimbus.features.shoppingExperience.value().enabled)
+ userHasOnboarded.set(settings.reviewQualityCheckOptInTimeInMillis != 0L)
+ disabledAds.set(!settings.isReviewQualityCheckProductRecommendationsEnabled)
+ }
+
+ TabStrip.enabled.set(settings.isTabletAndTabStripEnabled)
+ }
+
+ @VisibleForTesting
+ internal fun getDeviceTotalRAM(): Long {
+ val memoryInfo = getMemoryInfo()
+ return if (SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ memoryInfo.advertisedMem
+ } else {
+ memoryInfo.totalMem
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getMemoryInfo(): ActivityManager.MemoryInfo {
+ val memoryInfo = ActivityManager.MemoryInfo()
+ val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ activityManager.getMemoryInfo(memoryInfo)
+
+ return memoryInfo
+ }
+
+ private fun deviceRamApproxMegabytes(): Long {
+ val deviceRamBytes = getMemoryInfo().totalMem
+ return deviceRamBytes.toRoundedMegabytes()
+ }
+
+ private fun Long.toRoundedMegabytes(): Long = (this / BYTES_TO_MEGABYTES_CONVERSION).roundToLong()
+
+ private fun isDeviceRamAboveThreshold() = deviceRamApproxMegabytes() > RAM_THRESHOLD_MEGABYTES
+
+ @Suppress("ComplexMethod")
+ private fun setPreferenceMetrics(
+ settings: Settings,
+ ) {
+ with(Preferences) {
+ searchSuggestionsEnabled.set(settings.shouldShowSearchSuggestions)
+ remoteDebuggingEnabled.set(settings.isRemoteDebuggingEnabled)
+ studiesEnabled.set(settings.isExperimentationEnabled)
+ telemetryEnabled.set(settings.isTelemetryEnabled)
+ browsingHistorySuggestion.set(settings.shouldShowHistorySuggestions)
+ bookmarksSuggestion.set(settings.shouldShowBookmarkSuggestions)
+ clipboardSuggestionsEnabled.set(settings.shouldShowClipboardSuggestions)
+ searchShortcutsEnabled.set(settings.shouldShowSearchShortcuts)
+ voiceSearchEnabled.set(settings.shouldShowVoiceSearch)
+ openLinksInAppEnabled.set(settings.openLinksInExternalApp)
+ signedInSync.set(settings.signedInFxaAccount)
+
+ val syncedItems = SyncEnginesStorage(applicationContext).getStatus().entries.filter {
+ it.value
+ }.map { it.key.nativeName }
+ syncItems.set(syncedItems)
+
+ toolbarPositionSetting.set(
+ when {
+ settings.shouldUseFixedTopToolbar -> "fixed_top"
+ settings.shouldUseBottomToolbar -> "bottom"
+ else -> "top"
+ },
+ )
+
+ enhancedTrackingProtection.set(
+ when {
+ !settings.shouldUseTrackingProtection -> ""
+ settings.useStandardTrackingProtection -> "standard"
+ settings.useStrictTrackingProtection -> "strict"
+ settings.useCustomTrackingProtection -> "custom"
+ else -> ""
+ },
+ )
+ etpCustomCookiesSelection.set(settings.blockCookiesSelectionInCustomTrackingProtection)
+
+ val accessibilitySelection = mutableListOf()
+
+ if (settings.switchServiceIsEnabled) {
+ accessibilitySelection.add("switch")
+ }
+
+ if (settings.touchExplorationIsEnabled) {
+ accessibilitySelection.add("touch exploration")
+ }
+
+ accessibilityServices.set(accessibilitySelection.toList())
+
+ userTheme.set(
+ when {
+ settings.shouldUseLightTheme -> "light"
+ settings.shouldUseDarkTheme -> "dark"
+ settings.shouldFollowDeviceTheme -> "system"
+ settings.shouldUseAutoBatteryTheme -> "battery"
+ else -> ""
+ },
+ )
+
+ inactiveTabsEnabled.set(settings.inactiveTabsAreEnabled)
+ }
+ reportHomeScreenMetrics(settings)
+ }
+
+ @VisibleForTesting
+ internal fun reportHomeScreenMetrics(settings: Settings) {
+ reportOpeningScreenMetrics(settings)
+ reportHomeScreenSectionMetrics(settings)
+ }
+
+ private fun reportOpeningScreenMetrics(settings: Settings) {
+ CustomizeHome.openingScreen.set(
+ when {
+ settings.alwaysOpenTheHomepageWhenOpeningTheApp -> "homepage"
+ settings.alwaysOpenTheLastTabWhenOpeningTheApp -> "last tab"
+ settings.openHomepageAfterFourHoursOfInactivity -> "homepage after four hours"
+ else -> ""
+ },
+ )
+ }
+
+ private fun reportHomeScreenSectionMetrics(settings: Settings) {
+ // These settings are backed by Nimbus features.
+ // We break them out here so they can be recorded when
+ // `nimbus.applyPendingExperiments()` is called.
+ CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature)
+ CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature)
+ CustomizeHome.mostVisitedSites.set(settings.showTopSitesFeature)
+ CustomizeHome.recentlyVisited.set(settings.historyMetadataUIFeature)
+ CustomizeHome.pocket.set(settings.showPocketRecommendationsFeature)
+ CustomizeHome.sponsoredPocket.set(settings.showPocketSponsoredStories)
+ CustomizeHome.contile.set(settings.showContileFeature)
+ }
+
+ protected fun recordOnInit() {
+ // This gets called by more than one process. Ideally we'd only run this in the main process
+ // but the code to check which process we're in crashes because the Context isn't valid yet.
+ //
+ // This method is not covered by our internal crash reporting: be very careful when modifying it.
+ StartupTimeline.onApplicationInit() // DO NOT MOVE ANYTHING ABOVE HERE: the timing is critical.
+ }
+
+ override fun onConfigurationChanged(config: android.content.res.Configuration) {
+ // Workaround for androidx appcompat issue where follow system day/night mode config changes
+ // are not triggered when also using createConfigurationContext like we do in LocaleManager
+ // https://issuetracker.google.com/issues/143570309#comment3
+ applicationContext.resources.configuration.uiMode = config.uiMode
+
+ if (isMainProcess()) {
+ // We can only do this on the main process as resetAfter will access components.core, which
+ // will initialize the engine and create an additional GeckoRuntime from the Gecko
+ // child process, causing a crash.
+
+ // There's a strict mode violation in A-Cs LocaleAwareApplication which
+ // reads from shared prefs: https://github.com/mozilla-mobile/android-components/issues/8816
+ components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ super.onConfigurationChanged(config)
+ }
+ } else {
+ super.onConfigurationChanged(config)
+ }
+ }
+
+ override val workManagerConfiguration = Builder().setMinimumLoggingLevel(INFO).build()
+
+ @OptIn(DelicateCoroutinesApi::class)
+ open fun downloadWallpapers() {
+ GlobalScope.launch {
+ components.useCases.wallpaperUseCases.initialize()
+ }
+ }
+
+ /**
+ * Checks whether or not a privacy notice needs to be displayed before
+ * the application can continue to initialize.
+ */
+ internal fun shouldShowPrivacyNotice(): Boolean {
+ return Config.channel.isMozillaOnline &&
+ settings().shouldShowPrivacyPopWindow &&
+ !components.fenixOnboarding.userHasBeenOnboarded()
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixLogSink.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixLogSink.kt
new file mode 100644
index 0000000000..53e85688aa
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixLogSink.kt
@@ -0,0 +1,34 @@
+/* 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 mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.sink.AndroidLogSink
+import mozilla.components.support.base.log.sink.LogSink
+
+/**
+ * Fenix [LogSink] implementation that writes to Android's log, depending on settings.
+ *
+ * @param logsDebug If set to false, removes logging of debug logs.
+ * @param androidLogSink an [AndroidLogSink] that writes to Android's log.
+ */
+class FenixLogSink(
+ private val logsDebug: Boolean = true,
+ private val androidLogSink: LogSink,
+) : LogSink {
+
+ override fun log(
+ priority: Log.Priority,
+ tag: String?,
+ throwable: Throwable?,
+ message: String,
+ ) {
+ if (priority == Log.Priority.DEBUG && !logsDebug) {
+ return
+ }
+
+ androidLogSink.log(priority, tag, throwable, message)
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt
new file mode 100644
index 0000000000..08d0e5e1be
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix
+
+import androidx.navigation.NavDirections
+import mozilla.appservices.places.BookmarkRoot
+import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
+
+/**
+ * Used with [HomeActivity] global navigation to indicate which fragment is being opened.
+ *
+ * @property navDirections NavDirections to navigate to destination
+ * @property destinationId fragment ID of the fragment being navigated to
+ */
+enum class GlobalDirections(val navDirections: NavDirections, val destinationId: Int) {
+ Home(NavGraphDirections.actionGlobalHome(), R.id.homeFragment),
+ Bookmarks(
+ NavGraphDirections.actionGlobalBookmarkFragment(BookmarkRoot.Root.id),
+ R.id.bookmarkFragment,
+ ),
+ History(
+ NavGraphDirections.actionGlobalHistoryFragment(),
+ R.id.historyFragment,
+ ),
+ Settings(
+ NavGraphDirections.actionGlobalSettingsFragment(),
+ R.id.settingsFragment,
+ ),
+ Sync(
+ NavGraphDirections.actionGlobalTurnOnSync(entrypoint = FenixFxAEntryPoint.DeepLink),
+ R.id.turnOnSyncFragment,
+ ),
+ SearchEngine(
+ NavGraphDirections.actionGlobalSearchEngineFragment(),
+ R.id.searchEngineFragment,
+ ),
+ Accessibility(
+ NavGraphDirections.actionGlobalAccessibilityFragment(),
+ R.id.accessibilityFragment,
+ ),
+ DeleteData(
+ NavGraphDirections.actionGlobalDeleteBrowsingDataFragment(),
+ R.id.deleteBrowsingDataFragment,
+ ),
+ SettingsAddonManager(
+ NavGraphDirections.actionGlobalAddonsManagementFragment(),
+ R.id.addonsManagementFragment,
+ ),
+ SettingsLogins(
+ NavGraphDirections.actionGlobalSavedLoginsAuthFragment(),
+ R.id.saveLoginSettingFragment,
+ ),
+ SettingsTrackingProtection(
+ NavGraphDirections.actionGlobalTrackingProtectionFragment(),
+ R.id.trackingProtectionFragment,
+ ),
+ WallpaperSettings(
+ NavGraphDirections.actionGlobalWallpaperSettingsFragment(),
+ R.id.wallpaperSettingsFragment,
+ ),
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
new file mode 100644
index 0000000000..1b17a97507
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
@@ -0,0 +1,1271 @@
+/* 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.app.assist.AssistContent
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_MAIN
+import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
+import android.content.res.Configuration
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.StrictMode
+import android.text.TextUtils
+import android.text.format.DateUtils
+import android.util.AttributeSet
+import android.view.ActionMode
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.WindowManager.LayoutParams.FLAG_SECURE
+import androidx.annotation.CallSuper
+import androidx.annotation.IdRes
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.ActionBar
+import androidx.appcompat.widget.Toolbar
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.NavController
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.NavigationUI
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import mozilla.appservices.places.BookmarkRoot
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.search.SearchEngine
+import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
+import mozilla.components.browser.state.selector.selectedTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.WebExtensionState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.storage.HistoryMetadataKey
+import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
+import mozilla.components.feature.media.ext.findActiveMediaTab
+import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
+import mozilla.components.feature.search.BrowserStoreSearchAdapter
+import mozilla.components.service.fxa.sync.SyncReason
+import mozilla.components.support.base.feature.ActivityResultHandler
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
+import mozilla.components.support.ktx.android.content.call
+import mozilla.components.support.ktx.android.content.email
+import mozilla.components.support.ktx.android.content.share
+import mozilla.components.support.ktx.kotlin.isUrl
+import mozilla.components.support.ktx.kotlin.toNormalizedUrl
+import mozilla.components.support.locale.LocaleAwareAppCompatActivity
+import mozilla.components.support.utils.BootUtils
+import mozilla.components.support.utils.BrowsersCache
+import mozilla.components.support.utils.ManufacturerCodes
+import mozilla.components.support.utils.SafeIntent
+import mozilla.components.support.utils.toSafeIntent
+import mozilla.components.support.webextensions.WebExtensionPopupObserver
+import mozilla.telemetry.glean.private.NoExtras
+import org.mozilla.experiments.nimbus.initializeTooling
+import org.mozilla.fenix.GleanMetrics.AppIcon
+import org.mozilla.fenix.GleanMetrics.Events
+import org.mozilla.fenix.GleanMetrics.Metrics
+import org.mozilla.fenix.GleanMetrics.SplashScreen
+import org.mozilla.fenix.GleanMetrics.StartOnHome
+import org.mozilla.fenix.addons.ExtensionsProcessDisabledBackgroundController
+import org.mozilla.fenix.addons.ExtensionsProcessDisabledForegroundController
+import org.mozilla.fenix.browser.browsingmode.BrowsingMode
+import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
+import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
+import org.mozilla.fenix.components.appstate.AppAction
+import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
+import org.mozilla.fenix.components.metrics.GrowthDataWorker
+import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
+import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
+import org.mozilla.fenix.databinding.ActivityHomeBinding
+import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
+import org.mozilla.fenix.debugsettings.ui.FenixOverlay
+import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
+import org.mozilla.fenix.ext.alreadyOnDestination
+import org.mozilla.fenix.ext.breadcrumb
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.getBreadcrumbMessage
+import org.mozilla.fenix.ext.getIntentSessionId
+import org.mozilla.fenix.ext.getIntentSource
+import org.mozilla.fenix.ext.getNavDirections
+import org.mozilla.fenix.ext.hasTopDestination
+import org.mozilla.fenix.ext.nav
+import org.mozilla.fenix.ext.setNavigationIcon
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.extension.WebExtensionPromptFeature
+import org.mozilla.fenix.home.intent.AssistIntentProcessor
+import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
+import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor
+import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
+import org.mozilla.fenix.home.intent.OpenPasswordManagerIntentProcessor
+import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
+import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor
+import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
+import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
+import org.mozilla.fenix.library.bookmarks.DesktopFolders
+import org.mozilla.fenix.messaging.FenixMessageSurfaceId
+import org.mozilla.fenix.messaging.MessageNotificationWorker
+import org.mozilla.fenix.nimbus.FxNimbus
+import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
+import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
+import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
+import org.mozilla.fenix.perf.Performance
+import org.mozilla.fenix.perf.PerformanceInflater
+import org.mozilla.fenix.perf.ProfilerMarkers
+import org.mozilla.fenix.perf.StartupPathProvider
+import org.mozilla.fenix.perf.StartupTimeline
+import org.mozilla.fenix.perf.StartupTypeTelemetry
+import org.mozilla.fenix.session.PrivateNotificationService
+import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor.Companion.ACTION_OPEN_PRIVATE_TAB
+import org.mozilla.fenix.tabhistory.TabHistoryDialogFragment
+import org.mozilla.fenix.tabstray.TabsTrayFragment
+import org.mozilla.fenix.theme.DefaultThemeManager
+import org.mozilla.fenix.theme.ThemeManager
+import org.mozilla.fenix.utils.Settings
+import java.lang.ref.WeakReference
+import java.util.Locale
+
+/**
+ * The main activity of the application. The application is primarily a single Activity (this one)
+ * with fragments switching out to display different views. The most important views shown here are the:
+ * - home screen
+ * - browser screen
+ */
+@SuppressWarnings("TooManyFunctions", "LargeClass", "LongMethod")
+open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
+ private lateinit var binding: ActivityHomeBinding
+ lateinit var themeManager: ThemeManager
+ lateinit var browsingModeManager: BrowsingModeManager
+
+ private var isVisuallyComplete = false
+
+ private var privateNotificationObserver: PrivateNotificationFeature? =
+ null
+
+ private var isToolbarInflated = false
+
+ private val webExtensionPopupObserver by lazy {
+ WebExtensionPopupObserver(components.core.store, ::openPopup)
+ }
+
+ val webExtensionPromptFeature by lazy {
+ WebExtensionPromptFeature(
+ store = components.core.store,
+ context = this@HomeActivity,
+ fragmentManager = supportFragmentManager,
+ )
+ }
+
+ private val extensionsProcessDisabledForegroundController by lazy {
+ ExtensionsProcessDisabledForegroundController(this@HomeActivity)
+ }
+
+ private val extensionsProcessDisabledBackgroundController by lazy {
+ ExtensionsProcessDisabledBackgroundController(
+ browserStore = components.core.store,
+ appStore = components.appStore,
+ )
+ }
+
+ private val serviceWorkerSupport by lazy {
+ ServiceWorkerSupportFeature(this)
+ }
+
+ private var inflater: LayoutInflater? = null
+
+ private val navHost by lazy {
+ supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
+ }
+
+ private val externalSourceIntentProcessors by lazy {
+ listOf(
+ HomeDeepLinkIntentProcessor(this),
+ SpeechProcessingIntentProcessor(this, components.core.store),
+ AssistIntentProcessor(),
+ StartSearchIntentProcessor(),
+ OpenBrowserIntentProcessor(this, ::getIntentSessionId),
+ OpenSpecificTabIntentProcessor(this),
+ OpenPasswordManagerIntentProcessor(),
+ ReEngagementIntentProcessor(this, settings()),
+ )
+ }
+
+ // See onKeyDown for why this is necessary
+ private var backLongPressJob: Job? = null
+
+ private lateinit var navigationToolbar: Toolbar
+
+ // Tracker for contextual menu (Copy|Search|Select all|etc...)
+ private var actionMode: ActionMode? = null
+
+ private val startupPathProvider = StartupPathProvider()
+ private lateinit var startupTypeTelemetry: StartupTypeTelemetry
+
+ @Suppress("ComplexMethod")
+ final override fun onCreate(savedInstanceState: Bundle?) {
+ // DO NOT MOVE ANYTHING ABOVE THIS getProfilerTime CALL.
+ val startTimeProfiler = components.core.engine.profiler?.getProfilerTime()
+
+ // Setup nimbus-cli tooling. This is a NOOP when launching normally.
+ components.nimbus.sdk.initializeTooling(applicationContext, intent)
+ components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
+ MarkersFragmentLifecycleCallbacks.register(supportFragmentManager, components.core.engine)
+
+ maybeShowSplashScreen()
+
+ // There is disk read violations on some devices such as samsung and pixel for android 9/10
+ components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ // Browsing mode & theme setup should always be called before super.onCreate.
+ setupBrowsingMode(getModeFromIntentOrLastKnown(intent))
+ setupTheme()
+
+ super.onCreate(savedInstanceState)
+ }
+
+ // Checks if Activity is currently in PiP mode if launched from external intents, then exits it
+ checkAndExitPiP()
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onCreate()",
+ data = mapOf(
+ "recreated" to (savedInstanceState != null).toString(),
+ "intent" to (intent?.action ?: "null"),
+ ),
+ )
+
+ components.publicSuffixList.prefetch()
+
+ // Changing a language on the Language screen restarts the activity, but the activity keeps
+ // the old layout direction. We have to update the direction manually.
+ window.decorView.layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+
+ binding = ActivityHomeBinding.inflate(layoutInflater)
+
+ lifecycleScope.launch {
+ val debugSettingsRepository = DefaultDebugSettingsRepository(
+ context = this@HomeActivity,
+ writeScope = this,
+ )
+
+ debugSettingsRepository.debugDrawerEnabled
+ .distinctUntilChanged()
+ .collect { enabled ->
+ with(binding.debugOverlay) {
+ if (enabled) {
+ visibility = View.VISIBLE
+
+ setContent {
+ FenixOverlay(
+ browserStore = components.core.store,
+ inactiveTabsEnabled = settings().inactiveTabsAreEnabled,
+ loginsStorage = components.core.passwordsStorage,
+ )
+ }
+ } else {
+ setContent {}
+
+ visibility = View.GONE
+ }
+ }
+ }
+ }
+
+ setContentView(binding.root)
+ ProfilerMarkers.addListenerForOnGlobalLayout(components.core.engine, this, binding.root)
+
+ // Must be after we set the content view
+ if (isVisuallyComplete) {
+ components.performance.visualCompletenessQueue
+ .attachViewToRunVisualCompletenessQueueLater(WeakReference(binding.rootContainer))
+ }
+
+ privateNotificationObserver = PrivateNotificationFeature(
+ applicationContext,
+ components.core.store,
+ PrivateNotificationService::class,
+ ).also {
+ it.start()
+ }
+
+ if (settings().shouldShowOnboarding(
+ hasUserBeenOnboarded = components.fenixOnboarding.userHasBeenOnboarded(),
+ isLauncherIntent = intent.toSafeIntent().isLauncherIntent,
+ )
+ ) {
+ // Unless activity is recreated due to config change, navigate to onboarding
+ if (savedInstanceState == null) {
+ navHost.navController.navigate(NavGraphDirections.actionGlobalOnboarding())
+ }
+ } else {
+ lifecycleScope.launch(IO) {
+ showFullscreenMessageIfNeeded(applicationContext)
+ }
+
+ // Unless the activity is recreated, navigate to home first (without rendering it)
+ // to add it to the back stack.
+ if (savedInstanceState == null) {
+ navigateToHome(navHost.navController)
+ }
+
+ if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) {
+ navigateToBrowserOnColdStart()
+ } else {
+ StartOnHome.enterHomeScreen.record(NoExtras())
+ }
+
+ if (settings().showHomeOnboardingDialog && components.fenixOnboarding.userHasBeenOnboarded()) {
+ navHost.navController.navigate(NavGraphDirections.actionGlobalHomeOnboardingDialog())
+ }
+ }
+
+ Performance.processIntentIfPerformanceTest(intent, this)
+
+ if (settings().isTelemetryEnabled) {
+ lifecycle.addObserver(
+ BreadcrumbsRecorder(
+ components.analytics.crashReporter,
+ navHost.navController,
+ ::getBreadcrumbMessage,
+ ),
+ )
+
+ val safeIntent = intent?.toSafeIntent()
+ safeIntent
+ ?.let(::getIntentSource)
+ ?.also {
+ Events.appOpened.record(Events.AppOpenedExtra(it))
+ // This will record an event in Nimbus' internal event store. Used for behavioral targeting
+ components.nimbus.events.recordEvent("app_opened")
+
+ if (safeIntent.action.equals(ACTION_OPEN_PRIVATE_TAB) && it == APP_ICON) {
+ AppIcon.newPrivateTabTapped.record(NoExtras())
+ }
+ }
+ }
+ supportActionBar?.hide()
+
+ lifecycle.addObservers(
+ webExtensionPopupObserver,
+ extensionsProcessDisabledForegroundController,
+ extensionsProcessDisabledBackgroundController,
+ serviceWorkerSupport,
+ webExtensionPromptFeature,
+ )
+
+ if (shouldAddToRecentsScreen(intent)) {
+ intent.removeExtra(START_IN_RECENTS_SCREEN)
+ moveTaskToBack(true)
+ }
+
+ captureSnapshotTelemetryMetrics()
+
+ startupTelemetryOnCreateCalled(intent.toSafeIntent())
+ startupPathProvider.attachOnActivityOnCreate(lifecycle, intent)
+ startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply {
+ attachOnHomeActivityOnCreate(lifecycle)
+ }
+
+ components.core.requestInterceptor.setNavigationController(navHost.navController)
+
+ if (settings().showContileFeature) {
+ components.core.contileTopSitesUpdater.startPeriodicWork()
+ }
+
+ if (!settings().hiddenEnginesRestored) {
+ settings().hiddenEnginesRestored = true
+ components.useCases.searchUseCases.restoreHiddenSearchEngines.invoke()
+ }
+
+ // To assess whether the Pocket stories are to be downloaded or not multiple SharedPreferences
+ // are read possibly needing to load them on the current thread. Move that to a background thread.
+ lifecycleScope.launch(IO) {
+ if (settings().showPocketRecommendationsFeature) {
+ components.core.pocketStoriesService.startPeriodicStoriesRefresh()
+ }
+ if (settings().showPocketSponsoredStories) {
+ components.core.pocketStoriesService.startPeriodicSponsoredStoriesRefresh()
+ // If the secret setting for sponsored stories parameters is set,
+ // force refresh the sponsored Pocket stories.
+ if (settings().useCustomConfigurationForSponsoredStories) {
+ components.core.pocketStoriesService.refreshSponsoredStories()
+ }
+ }
+ }
+
+ components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
+ lifecycleScope.launch(IO) {
+ // If we're authenticated, kick-off a sync and a device state refresh.
+ components.backgroundServices.accountManager.authenticatedAccount()?.let {
+ components.backgroundServices.accountManager.syncNow(reason = SyncReason.Startup)
+ }
+ }
+ }
+
+ components.core.engine.profiler?.addMarker(
+ MarkersActivityLifecycleCallbacks.MARKER_NAME,
+ startTimeProfiler,
+ "HomeActivity.onCreate",
+ )
+
+ components.notificationsDelegate.bindToActivity(this)
+
+ StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
+ }
+
+ private fun maybeShowSplashScreen() {
+ if (components.settings.isFirstSplashScreenShown) {
+ return
+ } else {
+ components.settings.isFirstSplashScreenShown = true
+ // Splash screen compat fails to draw icons on earlier versions.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+ return
+ }
+ }
+
+ if (FxNimbus.features.splashScreen.value().enabled) {
+ val splashScreen = installSplashScreen()
+ var maxDurationReached = false
+ val delay = FxNimbus.features.splashScreen.value().maximumDurationMs.toLong()
+ splashScreen.setKeepOnScreenCondition {
+ val dataFetched = components.settings.nimbusExperimentsFetched
+
+ val keepOnScreen = !maxDurationReached && !dataFetched
+ if (!keepOnScreen) {
+ SplashScreen.firstLaunchExtended.record(
+ SplashScreen.FirstLaunchExtendedExtra(dataFetched = dataFetched),
+ )
+ }
+ keepOnScreen
+ }
+ MainScope().launch {
+ delay(timeMillis = delay)
+ maxDurationReached = true
+ }
+ }
+ }
+
+ private fun checkAndExitPiP() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode && intent != null) {
+ // Exit PiP mode
+ moveTaskToBack(false)
+ startActivity(Intent(this, this::class.java).setFlags(FLAG_ACTIVITY_REORDER_TO_FRONT))
+ }
+ }
+
+ private fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent) {
+ // We intentionally only record this in HomeActivity and not ExternalBrowserActivity (e.g.
+ // PWAs) so we don't include more unpredictable code paths in the results.
+ components.performance.coldStartupDurationTelemetry.onHomeActivityOnCreate(
+ components.performance.visualCompletenessQueue,
+ components.startupStateProvider,
+ safeIntent,
+ binding.rootContainer,
+ )
+ }
+
+ @CallSuper
+ @Suppress("TooGenericExceptionCaught")
+ override fun onResume() {
+ super.onResume()
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onResume()",
+ )
+
+ lifecycleScope.launch(IO) {
+ try {
+ if (settings().showContileFeature) {
+ components.core.contileTopSitesProvider.refreshTopSitesIfCacheExpired()
+ }
+ } catch (e: Exception) {
+ Logger.error("Failed to refresh contile top sites", e)
+ }
+
+ if (settings().checkIfFenixIsDefaultBrowserOnAppResume()) {
+ Events.defaultBrowserChanged.record(NoExtras())
+ }
+
+ GrowthDataWorker.sendActivatedSignalIfNeeded(applicationContext)
+ FontEnumerationWorker.sendActivatedSignalIfNeeded(applicationContext)
+ ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
+ MessageNotificationWorker.setMessageNotificationWorker(applicationContext)
+ }
+
+ // This was done in order to refresh search engines when app is running in background
+ // and the user changes the system language
+ // More details here: https://github.com/mozilla-mobile/fenix/pull/27793#discussion_r1029892536
+ components.core.store.dispatch(SearchAction.RefreshSearchEnginesAction)
+ }
+
+ final override fun onStart() {
+ // DO NOT MOVE ANYTHING ABOVE THIS getProfilerTime CALL.
+ val startProfilerTime = components.core.engine.profiler?.getProfilerTime()
+
+ super.onStart()
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onStart()",
+ )
+
+ ProfilerMarkers.homeActivityOnStart(binding.rootContainer, components.core.engine.profiler)
+ components.core.engine.profiler?.addMarker(
+ MarkersActivityLifecycleCallbacks.MARKER_NAME,
+ startProfilerTime,
+ "HomeActivity.onStart",
+ ) // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL.
+ }
+
+ final override fun onStop() {
+ super.onStop()
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onStop()",
+ data = mapOf(
+ "finishing" to isFinishing.toString(),
+ ),
+ )
+ }
+
+ final override fun onPause() {
+ // We should return to the browser if there were normal tabs when we left the app
+ settings().shouldReturnToBrowser =
+ components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty()
+
+ lifecycleScope.launch(IO) {
+ val desktopFolders = DesktopFolders(
+ applicationContext,
+ showMobileRoot = false,
+ )
+ settings().desktopBookmarksSize = desktopFolders.count()
+
+ settings().mobileBookmarksSize = components.core.bookmarksStorage.countBookmarksInTrees(
+ listOf(BookmarkRoot.Mobile.id),
+ ).toInt()
+ }
+
+ super.onPause()
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onPause()",
+ data = mapOf(
+ "finishing" to isFinishing.toString(),
+ ),
+ )
+
+ // Every time the application goes into the background, it is possible that the user
+ // is about to change the browsers installed on their system. Therefore, we reset the cache of
+ // all the installed browsers.
+ //
+ // NB: There are ways for the user to install new products without leaving the browser.
+ BrowsersCache.resetAll()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onProvideAssistContent(outContent: AssistContent?) {
+ super.onProvideAssistContent(outContent)
+ val currentTabUrl = components.core.store.state.selectedTab?.content?.url
+ outContent?.webUri = currentTabUrl?.let { Uri.parse(it) }
+ }
+
+ @CallSuper
+ override fun onDestroy() {
+ super.onDestroy()
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onDestroy()",
+ data = mapOf(
+ "finishing" to isFinishing.toString(),
+ ),
+ )
+
+ components.core.contileTopSitesUpdater.stopPeriodicWork()
+ components.core.pocketStoriesService.stopPeriodicStoriesRefresh()
+ components.core.pocketStoriesService.stopPeriodicSponsoredStoriesRefresh()
+ privateNotificationObserver?.stop()
+ components.notificationsDelegate.unBindActivity(this)
+
+ val activityStartedWithLink = startupPathProvider.startupPathForActivity == StartupPathProvider.StartupPath.VIEW
+ if (this !is ExternalAppBrowserActivity && !activityStartedWithLink) {
+ stopMediaSession()
+ }
+ }
+
+ final override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onConfigurationChanged()",
+ )
+ }
+
+ final override fun recreate() {
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "recreate()",
+ )
+
+ super.recreate()
+ }
+
+ /**
+ * Handles intents received when the activity is open.
+ */
+ final override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ intent?.let {
+ handleNewIntent(it)
+ }
+ startupPathProvider.onIntentReceived(intent)
+ }
+
+ @VisibleForTesting
+ internal fun handleNewIntent(intent: Intent) {
+ if (this is ExternalAppBrowserActivity) {
+ return
+ }
+
+ // Diagnostic breadcrumb for "Display already aquired" crash:
+ // https://github.com/mozilla-mobile/android-components/issues/7960
+ breadcrumb(
+ message = "onNewIntent()",
+ data = mapOf(
+ "intent" to intent.action.toString(),
+ ),
+ )
+
+ val tab = components.core.store.state.findActiveMediaTab()
+ if (tab != null) {
+ components.useCases.sessionUseCases.exitFullscreen(tab.id)
+ }
+
+ val intentProcessors =
+ listOf(
+ CrashReporterIntentProcessor(components.appStore),
+ ) + externalSourceIntentProcessors
+ val intentHandled =
+ intentProcessors.any { it.process(intent, navHost.navController, this.intent) }
+ browsingModeManager.mode = getModeFromIntentOrLastKnown(intent)
+
+ if (intentHandled) {
+ supportFragmentManager
+ .primaryNavigationFragment
+ ?.childFragmentManager
+ ?.fragments
+ ?.lastOrNull()
+ ?.let { it as? TabsTrayFragment }
+ ?.also { it.dismissAllowingStateLoss() }
+ }
+ }
+
+ /**
+ * Overrides view inflation to inject a custom [EngineView] from [components].
+ */
+ final override fun onCreateView(
+ parent: View?,
+ name: String,
+ context: Context,
+ attrs: AttributeSet,
+ ): View? = when (name) {
+ EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
+ selectionActionDelegate = DefaultSelectionActionDelegate(
+ BrowserStoreSearchAdapter(
+ components.core.store,
+ tabId = getIntentSessionId(intent.toSafeIntent()),
+ ),
+ resources = context.resources,
+ shareTextClicked = { share(it) },
+ emailTextClicked = { email(it) },
+ callTextClicked = { call(it) },
+ actionSorter = ::actionSorter,
+ )
+ }.asView()
+ else -> super.onCreateView(parent, name, context, attrs)
+ }
+
+ final override fun onActionModeStarted(mode: ActionMode?) {
+ actionMode = mode
+ super.onActionModeStarted(mode)
+ }
+
+ final override fun onActionModeFinished(mode: ActionMode?) {
+ actionMode = null
+ super.onActionModeFinished(mode)
+ }
+
+ fun finishActionMode() {
+ actionMode?.finish().also { actionMode = null }
+ }
+
+ @Suppress("MagicNumber")
+ // Defining the positions as constants doesn't seem super useful here.
+ private fun actionSorter(actions: Array): Array {
+ val order = hashMapOf()
+
+ order["CUSTOM_CONTEXT_MENU_EMAIL"] = 0
+ order["CUSTOM_CONTEXT_MENU_CALL"] = 1
+ order["org.mozilla.geckoview.COPY"] = 2
+ order["CUSTOM_CONTEXT_MENU_SEARCH"] = 3
+ order["CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"] = 4
+ order["org.mozilla.geckoview.PASTE"] = 5
+ order["org.mozilla.geckoview.SELECT_ALL"] = 6
+ order["CUSTOM_CONTEXT_MENU_SHARE"] = 7
+
+ return actions.sortedBy { actionName ->
+ // Sort the actions in our preferred order, putting "other" actions unsorted at the end
+ order[actionName] ?: actions.size
+ }.toTypedArray()
+ }
+
+ final override fun onBackPressed() {
+ supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
+ if (it is UserInteractionHandler && it.onBackPressed()) {
+ return
+ }
+ }
+ onBackPressedDispatcher.onBackPressed()
+ }
+
+ @Deprecated("Deprecated in Java")
+ // https://github.com/mozilla-mobile/fenix/issues/19919
+ final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
+ if (it is ActivityResultHandler && it.onActivityResult(requestCode, data, resultCode)) {
+ return
+ }
+ }
+ @Suppress("DEPRECATION")
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ private fun shouldUseCustomBackLongPress(): Boolean {
+ val isAndroidN =
+ Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
+ // Huawei devices seem to have problems with onKeyLongPress
+ // See https://github.com/mozilla-mobile/fenix/issues/13498
+ return isAndroidN || ManufacturerCodes.isHuawei
+ }
+
+ private fun handleBackLongPress(): Boolean {
+ supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
+ if (it is OnBackLongPressedListener && it.onBackLongPressed()) {
+ return true
+ }
+ }
+ return false
+ }
+
+ final override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
+ ProfilerMarkers.addForDispatchTouchEvent(components.core.engine.profiler, ev)
+ return super.dispatchTouchEvent(ev)
+ }
+
+ final override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ // Inspired by https://searchfox.org/mozilla-esr68/source/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java#584-613
+ // Android N and Huawei devices have broken onKeyLongPress events for the back button, so we
+ // instead implement the long press behavior ourselves
+ // - For short presses, we cancel the callback in onKeyUp
+ // - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere
+ // (but Android still provides the haptic feedback), and the long press action is run
+ if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
+ backLongPressJob = lifecycleScope.launch {
+ delay(ViewConfiguration.getLongPressTimeout().toLong())
+ handleBackLongPress()
+ }
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ final override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
+ if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
+ backLongPressJob?.cancel()
+
+ // check if the key has been pressed for longer than the time needed for a press to turn into a long press
+ // and if tab history is already visible we do not want to dismiss it.
+ if (event.eventTime - event.downTime >= ViewConfiguration.getLongPressTimeout() &&
+ navHost.navController.hasTopDestination(TabHistoryDialogFragment.NAME)
+ ) {
+ // returning true avoids further processing of the KeyUp event and avoids dismissing tab history.
+ return true
+ }
+ }
+ return super.onKeyUp(keyCode, event)
+ }
+
+ final override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
+ // onKeyLongPress is broken in Android N so we don't handle back button long presses here
+ // for N. The version check ensures we don't handle back button long presses twice.
+ if (!shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) {
+ return handleBackLongPress()
+ }
+ return super.onKeyLongPress(keyCode, event)
+ }
+
+ final override fun onUserLeaveHint() {
+ // The notification permission prompt will trigger onUserLeaveHint too.
+ // We shouldn't treat this situation as user leaving.
+ if (!components.notificationsDelegate.isRequestingPermission) {
+ supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
+ if (it is UserInteractionHandler && it.onHomePressed()) {
+ return
+ }
+ }
+ }
+
+ super.onUserLeaveHint()
+ }
+
+ /**
+ * External sources such as 3rd party links and shortcuts use this function to enter
+ * private mode directly before the content view is created. Returns the mode set by the intent
+ * otherwise falls back to the last known mode.
+ */
+ @VisibleForTesting
+ internal fun getModeFromIntentOrLastKnown(intent: Intent?): BrowsingMode {
+ intent?.toSafeIntent()?.let {
+ if (it.hasExtra(PRIVATE_BROWSING_MODE)) {
+ val startPrivateMode = it.getBooleanExtra(PRIVATE_BROWSING_MODE, false)
+ return BrowsingMode.fromBoolean(isPrivate = startPrivateMode)
+ }
+ }
+ return settings().lastKnownMode
+ }
+
+ /**
+ * Determines whether the activity should be pushed to be backstack (i.e., 'minimized' to the recents
+ * screen) upon starting.
+ * @param intent - The intent that started this activity. Is checked for having the 'START_IN_RECENTS_SCREEN'-extra.
+ * @return true if the activity should be started and pushed to the recents screen, false otherwise.
+ */
+ private fun shouldAddToRecentsScreen(intent: Intent?): Boolean {
+ intent?.toSafeIntent()?.let {
+ return it.getBooleanExtra(START_IN_RECENTS_SCREEN, false)
+ }
+ return false
+ }
+
+ private fun setupBrowsingMode(mode: BrowsingMode) {
+ settings().lastKnownMode = mode
+ browsingModeManager = createBrowsingModeManager(mode)
+ }
+
+ private fun setupTheme() {
+ themeManager = createThemeManager()
+ // ExternalAppBrowserActivity exclusively handles it's own theming unless in private mode.
+ if (this !is ExternalAppBrowserActivity || browsingModeManager.mode.isPrivate) {
+ themeManager.setActivityTheme(this)
+ themeManager.applyStatusBarTheme(this)
+ }
+ }
+
+ // Stop active media when activity is destroyed.
+ private fun stopMediaSession() {
+ if (isFinishing) {
+ components.core.store.state.tabs.forEach {
+ it.mediaSessionState?.controller?.stop()
+ }
+
+ components.core.store.state.findActiveMediaTab()?.let {
+ components.core.store.dispatch(
+ MediaSessionAction.DeactivatedMediaSessionAction(
+ it.id,
+ ),
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns the [supportActionBar], inflating it if necessary.
+ * Everyone should call this instead of supportActionBar.
+ */
+ final override fun getSupportActionBarAndInflateIfNecessary(): ActionBar {
+ if (!isToolbarInflated) {
+ navigationToolbar = binding.navigationToolbarStub.inflate() as Toolbar
+
+ setSupportActionBar(navigationToolbar)
+ // Add ids to this that we don't want to have a toolbar back button
+ setupNavigationToolbar()
+ setNavigationIcon(R.drawable.ic_back_button)
+
+ isToolbarInflated = true
+ }
+ return supportActionBar!!
+ }
+
+ @Suppress("SpreadOperator")
+ private fun setupNavigationToolbar(vararg topLevelDestinationIds: Int) {
+ NavigationUI.setupWithNavController(
+ navigationToolbar,
+ navHost.navController,
+ AppBarConfiguration.Builder(*topLevelDestinationIds).build(),
+ )
+
+ navigationToolbar.setNavigationOnClickListener {
+ onBackPressed()
+ }
+ }
+
+ /**
+ * Navigates to the browser fragment and loads a URL or performs a search (depending on the
+ * value of [searchTermOrURL]).
+ *
+ * @param searchTermOrURL The entered search term to search or URL to be loaded.
+ * @param newTab Whether or not to load the URL in a new tab.
+ * @param from The [BrowserDirection] to indicate which fragment the browser is being
+ * opened from.
+ * @param customTabSessionId Optional custom tab session ID if navigating from a custom tab.
+ * @param engine Optional [SearchEngine] to use when performing a search.
+ * @param forceSearch Whether or not to force performing a search.
+ * @param flags Flags that will be used when loading the URL (not applied to searches).
+ * @param requestDesktopMode Whether or not to request the desktop mode for the session.
+ * @param historyMetadata The [HistoryMetadataKey] of the new tab in case this tab
+ * was opened from history.
+ * @param additionalHeaders The extra headers to use when loading the URL.
+ */
+ fun openToBrowserAndLoad(
+ searchTermOrURL: String,
+ newTab: Boolean,
+ from: BrowserDirection,
+ customTabSessionId: String? = null,
+ engine: SearchEngine? = null,
+ forceSearch: Boolean = false,
+ flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ requestDesktopMode: Boolean = false,
+ historyMetadata: HistoryMetadataKey? = null,
+ additionalHeaders: Map? = null,
+ ) {
+ openToBrowser(from, customTabSessionId)
+ load(
+ searchTermOrURL = searchTermOrURL,
+ newTab = newTab,
+ engine = engine,
+ forceSearch = forceSearch,
+ flags = flags,
+ requestDesktopMode = requestDesktopMode,
+ historyMetadata = historyMetadata,
+ additionalHeaders = additionalHeaders,
+ )
+ }
+
+ fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
+ if (navHost.navController.alreadyOnDestination(R.id.browserFragment)) return
+ @IdRes val fragmentId = if (from.fragmentId != 0) from.fragmentId else null
+ val directions = getNavDirections(from, customTabSessionId)
+ if (directions != null) {
+ navHost.navController.nav(fragmentId, directions)
+ }
+ }
+
+ /**
+ * Loads a URL or performs a search (depending on the value of [searchTermOrURL]).
+ *
+ * @param searchTermOrURL The entered search term to search or URL to be loaded.
+ * @param newTab Whether or not to load the URL in a new tab.
+ * @param engine Optional [SearchEngine] to use when performing a search.
+ * @param forceSearch Whether or not to force performing a search.
+ * @param flags Flags that will be used when loading the URL (not applied to searches).
+ * @param requestDesktopMode Whether or not to request the desktop mode for the session.
+ * @param historyMetadata The [HistoryMetadataKey] of the new tab in case this tab
+ * was opened from history.
+ * @param additionalHeaders The extra headers to use when loading the URL.
+ */
+ private fun load(
+ searchTermOrURL: String,
+ newTab: Boolean,
+ engine: SearchEngine?,
+ forceSearch: Boolean,
+ flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
+ requestDesktopMode: Boolean = false,
+ historyMetadata: HistoryMetadataKey? = null,
+ additionalHeaders: Map? = null,
+ ) {
+ val startTime = components.core.engine.profiler?.getProfilerTime()
+ val mode = browsingModeManager.mode
+
+ val private = when (mode) {
+ BrowsingMode.Private -> true
+ BrowsingMode.Normal -> false
+ }
+
+ // In situations where we want to perform a search but have no search engine (e.g. the user
+ // has removed all of them, or we couldn't load any) we will pass searchTermOrURL to Gecko
+ // and let it try to load whatever was entered.
+ if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) {
+ val tabId = if (newTab) {
+ components.useCases.tabsUseCases.addTab(
+ url = searchTermOrURL.toNormalizedUrl(),
+ flags = flags,
+ private = private,
+ historyMetadata = historyMetadata,
+ )
+ } else {
+ components.useCases.sessionUseCases.loadUrl(
+ url = searchTermOrURL.toNormalizedUrl(),
+ flags = flags,
+ )
+ components.core.store.state.selectedTabId
+ }
+
+ if (requestDesktopMode && tabId != null) {
+ handleRequestDesktopMode(tabId)
+ }
+ } else {
+ if (newTab) {
+ val searchUseCase = if (mode.isPrivate) {
+ components.useCases.searchUseCases.newPrivateTabSearch
+ } else {
+ components.useCases.searchUseCases.newTabSearch
+ }
+ searchUseCase.invoke(
+ searchTerms = searchTermOrURL,
+ source = SessionState.Source.Internal.UserEntered,
+ selected = true,
+ searchEngine = engine,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ } else {
+ components.useCases.searchUseCases.defaultSearch.invoke(
+ searchTerms = searchTermOrURL,
+ searchEngine = engine,
+ flags = flags,
+ additionalHeaders = additionalHeaders,
+ )
+ }
+ }
+
+ if (components.core.engine.profiler?.isProfilerActive() == true) {
+ // Wrapping the `addMarker` method with `isProfilerActive` even though it's no-op when
+ // profiler is not active. That way, `text` argument will not create a string builder all the time.
+ components.core.engine.profiler?.addMarker(
+ "HomeActivity.load",
+ startTime,
+ "newTab: $newTab",
+ )
+ }
+ }
+
+ internal fun handleRequestDesktopMode(tabId: String) {
+ components.useCases.sessionUseCases.requestDesktopSite(true, tabId)
+ components.core.store.dispatch(ContentAction.UpdateDesktopModeAction(tabId, true))
+
+ // Reset preference value after opening the tab in desktop mode
+ settings().openNextTabInDesktopMode = false
+ }
+
+ @VisibleForTesting
+ internal fun navigateToBrowserOnColdStart() {
+ if (this is ExternalAppBrowserActivity) {
+ return
+ }
+
+ // Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
+ // except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
+ if (settings().shouldReturnToBrowser && !browsingModeManager.mode.isPrivate) {
+ // Navigate to home first (without rendering it) to add it to the back stack.
+ openToBrowser(BrowserDirection.FromGlobal, null)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun navigateToHome(navController: NavController) {
+ if (this is ExternalAppBrowserActivity) {
+ return
+ }
+
+ navController.navigate(NavGraphDirections.actionStartupHome())
+ }
+
+ final override fun attachBaseContext(base: Context) {
+ base.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ super.attachBaseContext(base)
+ }
+ }
+
+ final override fun getSystemService(name: String): Any? {
+ // Issue #17759 had a crash with the PerformanceInflater.kt on Android 5.0 and 5.1
+ // when using the TimePicker. Since the inflater was created for performance monitoring
+ // purposes and that we test on new android versions, this means that any difference in
+ // inflation will be caught on those devices.
+ if (LAYOUT_INFLATER_SERVICE == name && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
+ if (inflater == null) {
+ inflater = PerformanceInflater(LayoutInflater.from(baseContext), this)
+ }
+ return inflater
+ }
+ return super.getSystemService(name)
+ }
+
+ private fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
+ return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
+ updateSecureWindowFlags(newMode)
+ themeManager.currentTheme = newMode
+ }.also {
+ updateSecureWindowFlags(initialMode)
+ }
+ }
+
+ private fun updateSecureWindowFlags(mode: BrowsingMode = browsingModeManager.mode) {
+ if (mode == BrowsingMode.Private && !settings().allowScreenshotsInPrivateMode) {
+ window.addFlags(FLAG_SECURE)
+ } else {
+ window.clearFlags(FLAG_SECURE)
+ }
+ }
+
+ private fun createThemeManager(): ThemeManager {
+ return DefaultThemeManager(browsingModeManager.mode, this)
+ }
+
+ private fun openPopup(webExtensionState: WebExtensionState) {
+ val action = NavGraphDirections.actionGlobalWebExtensionActionPopupFragment(
+ webExtensionId = webExtensionState.id,
+ webExtensionTitle = webExtensionState.name,
+ )
+ navHost.navController.navigate(action)
+ }
+
+ /**
+ * The root container is null at this point, so let the HomeActivity know that
+ * we are visually complete.
+ */
+ fun setVisualCompletenessQueueReady() {
+ isVisuallyComplete = true
+ }
+
+ private fun captureSnapshotTelemetryMetrics() = CoroutineScope(IO).launch {
+ // PWA
+ val recentlyUsedPwaCount = components.core.webAppShortcutManager.recentlyUsedWebAppsCount(
+ activeThresholdMs = PWA_RECENTLY_USED_THRESHOLD,
+ )
+ if (recentlyUsedPwaCount == 0) {
+ Metrics.hasRecentPwas.set(false)
+ } else {
+ Metrics.hasRecentPwas.set(true)
+ // This metric's lifecycle is set to 'application', meaning that it gets reset upon
+ // application restart. Combined with the behaviour of the metric type itself (a growing counter),
+ // it's important that this metric is only set once per application's lifetime.
+ // Otherwise, we're going to over-count.
+ Metrics.recentlyUsedPwaCount.add(recentlyUsedPwaCount)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun isActivityColdStarted(startingIntent: Intent, activityIcicle: Bundle?): Boolean {
+ // First time opening this activity in the task.
+ // Cold start / start from Recents after back press.
+ return activityIcicle == null &&
+ // Activity was restarted from Recents after it was destroyed by Android while in background
+ // in cases of memory pressure / "Don't keep activities".
+ startingIntent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0
+ }
+
+ /**
+ * Indicates if the user should be redirected to the [BrowserFragment] or to the [HomeFragment],
+ * links from an external apps should always opened in the [BrowserFragment].
+ */
+ @VisibleForTesting
+ internal fun shouldStartOnHome(intent: Intent? = this.intent): Boolean {
+ return components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ // We only want to open on home when users tap the app,
+ // we want to ignore other cases when the app gets open by users clicking on links.
+ getSettings().shouldStartOnHome() && intent?.action == ACTION_MAIN
+ }
+ }
+
+ fun processIntent(intent: Intent): Boolean {
+ return externalSourceIntentProcessors.any {
+ it.process(
+ intent,
+ navHost.navController,
+ this.intent,
+ )
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getSettings(): Settings = settings()
+
+ private fun shouldNavigateToBrowserOnColdStart(savedInstanceState: Bundle?): Boolean {
+ return isActivityColdStarted(intent, savedInstanceState) &&
+ !processIntent(intent)
+ }
+
+ private suspend fun showFullscreenMessageIfNeeded(context: Context) {
+ val messaging = context.components.nimbus.messaging
+ val nextMessage = messaging.getNextMessage(FenixMessageSurfaceId.SURVEY) ?: return
+ val researchSurfaceDialogFragment = ResearchSurfaceDialogFragment.newInstance(
+ keyMessageText = nextMessage.text,
+ keyAcceptButtonText = nextMessage.buttonLabel,
+ keyDismissButtonText = null,
+ )
+
+ researchSurfaceDialogFragment.onAccept = {
+ processIntent(messaging.getIntentForMessage(nextMessage))
+ components.appStore.dispatch(AppAction.MessagingAction.MessageClicked(nextMessage))
+ }
+
+ researchSurfaceDialogFragment.onDismiss = {
+ components.appStore.dispatch(AppAction.MessagingAction.MessageDismissed(nextMessage))
+ }
+
+ lifecycleScope.launch(Main) {
+ researchSurfaceDialogFragment.showNow(
+ supportFragmentManager,
+ ResearchSurfaceDialogFragment.FRAGMENT_TAG,
+ )
+ }
+
+ // Update message as displayed.
+ val currentBootUniqueIdentifier = BootUtils.getBootIdentifier(context)
+
+ messaging.onMessageDisplayed(nextMessage, currentBootUniqueIdentifier)
+ }
+
+ companion object {
+ const val OPEN_TO_BROWSER = "open_to_browser"
+ const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
+ const val OPEN_TO_SEARCH = "open_to_search"
+ const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
+ const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
+ const val OPEN_PASSWORD_MANAGER = "open_password_manager"
+ const val APP_ICON = "APP_ICON"
+
+ // PWA must have been used within last 30 days to be considered "recently used" for the
+ // telemetry purposes.
+ private const val PWA_RECENTLY_USED_THRESHOLD = DateUtils.DAY_IN_MILLIS * 30L
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt
new file mode 100644
index 0000000000..2ebb6bd3e6
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt
@@ -0,0 +1,161 @@
+/* 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.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.os.StrictMode
+import androidx.annotation.VisibleForTesting
+import mozilla.components.feature.intent.ext.sanitize
+import mozilla.components.feature.intent.processing.IntentProcessor
+import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
+import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
+import mozilla.components.support.utils.INTENT_TYPE_PDF
+import mozilla.components.support.utils.ext.getApplicationInfoCompat
+import org.mozilla.fenix.GleanMetrics.Events
+import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
+import org.mozilla.fenix.components.IntentProcessorType
+import org.mozilla.fenix.components.getType
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
+import org.mozilla.fenix.perf.StartupTimeline
+import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor
+
+/**
+ * Processes incoming intents and sends them to the corresponding activity.
+ */
+class IntentReceiverActivity : Activity() {
+
+ @VisibleForTesting
+ override fun onCreate(savedInstanceState: Bundle?) {
+ // DO NOT MOVE ANYTHING ABOVE THIS getProfilerTime CALL.
+ val startTimeProfiler = components.core.engine.profiler?.getProfilerTime()
+
+ // StrictMode violation on certain devices such as Samsung
+ components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ super.onCreate(savedInstanceState)
+ }
+
+ // The intent property is nullable, but the rest of the code below
+ // assumes it is not. If it's null, then we make a new one and open
+ // the HomeActivity.
+ val intent = intent?.let { Intent(it) } ?: Intent()
+ intent.sanitize().stripUnwantedFlags()
+ processIntent(intent)
+
+ components.core.engine.profiler?.addMarker(
+ MarkersActivityLifecycleCallbacks.MARKER_NAME,
+ startTimeProfiler,
+ "IntentReceiverActivity.onCreate",
+ )
+ StartupTimeline.onActivityCreateEndIntentReceiver() // DO NOT MOVE ANYTHING BELOW HERE.
+ }
+
+ fun processIntent(intent: Intent) {
+ // Call process for side effects, short on the first that returns true
+
+ var private = settings().openLinksInAPrivateTab
+ if (!private) {
+ // if PRIVATE_BROWSING_MODE is already set to true, honor that
+ private = intent.getBooleanExtra(PRIVATE_BROWSING_MODE, false)
+ }
+ intent.putExtra(PRIVATE_BROWSING_MODE, private)
+ if (private) {
+ Events.openedLink.record(Events.OpenedLinkExtra("PRIVATE"))
+ } else {
+ Events.openedLink.record(Events.OpenedLinkExtra("NORMAL"))
+ }
+
+ addReferrerInformation(intent)
+
+ if (intent.type == INTENT_TYPE_PDF) {
+ val referrerIsFenix =
+ intent.getStringExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE) == this.packageName
+ Events.openedExtPdf.record(Events.OpenedExtPdfExtra(referrerIsFenix))
+ }
+
+ val processor = getIntentProcessors(private).firstOrNull { it.process(intent) }
+ val intentProcessorType = components.intentProcessors.getType(processor)
+
+ launch(intent, intentProcessorType)
+ }
+
+ @VisibleForTesting
+ internal fun launch(intent: Intent, intentProcessorType: IntentProcessorType) {
+ intent.setClassName(applicationContext, intentProcessorType.activityClassName)
+
+ if (!intent.hasExtra(HomeActivity.OPEN_TO_BROWSER)) {
+ intent.putExtra(
+ HomeActivity.OPEN_TO_BROWSER,
+ intentProcessorType.shouldOpenToBrowser(intent),
+ )
+ }
+ // StrictMode violation on certain devices such as Samsung
+ components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
+ startActivity(intent)
+ }
+ finish() // must finish() after starting the other activity
+ }
+
+ private fun getIntentProcessors(private: Boolean): List {
+ val modeDependentProcessors = if (private) {
+ listOf(
+ components.intentProcessors.privateCustomTabIntentProcessor,
+ components.intentProcessors.privateIntentProcessor,
+ )
+ } else {
+ Events.openedLink.record(Events.OpenedLinkExtra("NORMAL"))
+ listOf(
+ components.intentProcessors.customTabIntentProcessor,
+ components.intentProcessors.intentProcessor,
+ )
+ }
+
+ return components.intentProcessors.externalAppIntentProcessors +
+ components.intentProcessors.fennecPageShortcutIntentProcessor +
+ components.intentProcessors.externalDeepLinkIntentProcessor +
+ components.intentProcessors.webNotificationsIntentProcessor +
+ components.intentProcessors.passwordManagerIntentProcessor +
+ modeDependentProcessors +
+ NewTabShortcutIntentProcessor()
+ }
+
+ private fun addReferrerInformation(intent: Intent) {
+ // Pass along referrer information when possible.
+ // Referrer is supported for API>=22.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ return
+ }
+ // NB: referrer can be spoofed by the calling application. Use with caution.
+ val r = referrer ?: return
+ intent.putExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE, r.host)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // Category is supported for API>=26.
+ r.host?.let { host ->
+ try {
+ val category = packageManager.getApplicationInfoCompat(host, 0).category
+ intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category)
+ } catch (e: PackageManager.NameNotFoundException) {
+ // At least we tried.
+ }
+ }
+ }
+ }
+}
+
+private fun Intent.stripUnwantedFlags() {
+ // Explicitly remove the new task and clear task flags (Our browser activity is a single
+ // task activity and we never want to start a second task here).
+ flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
+ flags = flags and Intent.FLAG_ACTIVITY_CLEAR_TASK.inv()
+
+ // IntentReceiverActivity is started with the "excludeFromRecents" flag (set in manifest). We
+ // do not want to propagate this flag from the intent receiver activity to the browser.
+ flags = flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.inv()
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/MozillaOnlineHomeActivity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/MozillaOnlineHomeActivity.kt
new file mode 100644
index 0000000000..63711f0f25
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/MozillaOnlineHomeActivity.kt
@@ -0,0 +1,29 @@
+/* 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.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
+
+/**
+ * This activity is specific to the Mozilla Online build and used to display
+ * a privacy notice on first run. Once the privacy notice is accepted, and for
+ * all subsequent launches, it will simply launch the Fenix [HomeActivity].
+ */
+class MozillaOnlineHomeActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if ((this.application as FenixApplication).shouldShowPrivacyNotice()) {
+ showPrivacyPopWindow(this.applicationContext, this)
+ } else {
+ startActivity(Intent(this, HomeActivity::class.java))
+ finish()
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/NavHostActivity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/NavHostActivity.kt
new file mode 100644
index 0000000000..a194c0eb4a
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/NavHostActivity.kt
@@ -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
+
+import androidx.appcompat.app.ActionBar
+
+/**
+ * Interface for the main activity in a single-activity architecture.
+ * All fragments will be displayed inside this activity.
+ */
+interface NavHostActivity {
+
+ /**
+ * Returns the support action bar, inflating it if necessary.
+ * Everyone should call this instead of supportActionBar.
+ */
+ fun getSupportActionBarAndInflateIfNecessary(): ActionBar
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt
new file mode 100644
index 0000000000..e47a0b71b4
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt
@@ -0,0 +1,21 @@
+/* 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
+
+/**
+ * Interface for features and fragments that want to handle long presses of the system back button
+ */
+interface OnBackLongPressedListener {
+
+ /**
+ * Called when the system back button is long pressed.
+ *
+ * Note: This cannot be called when gesture navigation is enabled on Android 10+ due to system
+ * limitations.
+ *
+ * @return true if the event was handled
+ */
+ fun onBackLongPressed(): Boolean
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/SecureFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/SecureFragment.kt
new file mode 100644
index 0000000000..24d2edf3c1
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/SecureFragment.kt
@@ -0,0 +1,34 @@
+/* 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.os.Bundle
+import androidx.annotation.LayoutRes
+import androidx.fragment.app.Fragment
+import org.mozilla.fenix.ext.removeSecure
+import org.mozilla.fenix.ext.secure
+
+/**
+ * A [Fragment] implementation that can be used to secure screens displaying sensitive information
+ * by not allowing taking screenshots of their content.
+ *
+ * Fragments displaying such screens should extend [SecureFragment] instead of [Fragment] class.
+ */
+open class SecureFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) {
+
+ constructor() : this(0) {
+ Fragment()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ this.secure()
+ super.onCreate(savedInstanceState)
+ }
+
+ override fun onDestroy() {
+ this.removeSecure()
+ super.onDestroy()
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ServiceWorkerSupportFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ServiceWorkerSupportFeature.kt
new file mode 100644
index 0000000000..574d77831c
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ServiceWorkerSupportFeature.kt
@@ -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
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
+import mozilla.components.concept.engine.serviceworker.ServiceWorkerDelegate
+import org.mozilla.fenix.ext.components
+
+/**
+ * Fenix own version of the `ServiceWorkerSupportFeature` from Android-Components
+ * which adds the ability to navigate to the browser before opening a new tab.
+ *
+ * Will automatically register callbacks for service workers requests and cleanup when [homeActivity] is destroyed.
+ *
+ * @param homeActivity [HomeActivity] used for navigating to browser or accessing various app components.
+ */
+class ServiceWorkerSupportFeature(
+ private val homeActivity: HomeActivity,
+) : ServiceWorkerDelegate, DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ homeActivity.components.core.engine.unregisterServiceWorkerDelegate()
+ }
+
+ override fun onCreate(owner: LifecycleOwner) {
+ homeActivity.components.core.engine.registerServiceWorkerDelegate(this)
+ }
+
+ override fun addNewTab(engineSession: EngineSession): Boolean {
+ with(homeActivity) {
+ openToBrowser(BrowserDirection.FromHome)
+
+ components.useCases.tabsUseCases.addTab(
+ flags = LoadUrlFlags.external(),
+ engineSession = engineSession,
+ source = SessionState.Source.Internal.None,
+ )
+ }
+
+ return true
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/StartupFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/StartupFragment.kt
new file mode 100644
index 0000000000..d12fc51af5
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/StartupFragment.kt
@@ -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
+
+import androidx.fragment.app.Fragment
+import org.mozilla.fenix.home.HomeFragment
+
+/**
+ * This empty fragment serves as a start destination in our navigation
+ * graph. It contains no layout and is fast to create compared to our
+ * [HomeFragment], which would otherwise be the start destination.
+ *
+ * When our [HomeActivity] is created we make a decision which fragment
+ * to navigate to, which makes sure we only render the [HomeFragment]
+ * as needed.
+ */
+class StartupFragment : Fragment()
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsBindingDelegate.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsBindingDelegate.kt
new file mode 100644
index 0000000000..3cfcc9575c
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsBindingDelegate.kt
@@ -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.addons
+
+import android.net.Uri
+import android.text.SpannableStringBuilder
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.text.style.URLSpan
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import androidx.core.text.HtmlCompat
+import androidx.core.text.getSpans
+import androidx.core.view.isVisible
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.translateDescription
+import mozilla.components.feature.addons.ui.updatedAtDate
+import mozilla.components.support.ktx.android.content.getColorFromAttr
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.FragmentAddOnDetailsBinding
+import org.mozilla.fenix.ext.addUnderline
+import java.text.DateFormat
+import java.text.NumberFormat
+import java.util.Locale
+
+interface AddonDetailsInteractor {
+
+ /**
+ * Open the given URL in the browser.
+ */
+ fun openWebsite(url: Uri)
+
+ /**
+ * Display the updater dialog.
+ */
+ fun showUpdaterDialog(addon: Addon)
+}
+
+/**
+ * Shows the details of an add-on.
+ */
+class AddonDetailsBindingDelegate(
+ private val binding: FragmentAddOnDetailsBinding,
+ private val interactor: AddonDetailsInteractor,
+) {
+
+ private val dateFormatter = DateFormat.getDateInstance()
+ private val numberFormatter = NumberFormat.getNumberInstance(Locale.getDefault())
+
+ fun bind(addon: Addon) {
+ bindDetails(addon)
+ bindAuthor(addon)
+ bindVersion(addon)
+ bindLastUpdated(addon)
+ bindHomepage(addon)
+ bindRating(addon)
+ bindDetailUrl(addon)
+ }
+
+ private fun bindRating(addon: Addon) {
+ addon.rating?.let { rating ->
+ val resources = binding.root.resources
+ val ratingContentDescription =
+ resources.getString(R.string.mozac_feature_addons_rating_content_description_2)
+ binding.ratingLabel.contentDescription = String.format(ratingContentDescription, rating.average)
+ binding.ratingView.rating = rating.average
+
+ val reviewCount = resources.getString(R.string.mozac_feature_addons_user_rating_count_2)
+ binding.reviewCount.contentDescription = String.format(reviewCount, numberFormatter.format(rating.reviews))
+ binding.reviewCount.text = numberFormatter.format(rating.reviews)
+
+ if (addon.ratingUrl.isNotBlank()) {
+ binding.reviewCount.setTextColor(binding.root.context.getColorFromAttr(R.attr.textAccent))
+ binding.reviewCount.addUnderline()
+ binding.reviewCount.setOnClickListener {
+ interactor.openWebsite(addon.ratingUrl.toUri())
+ }
+ }
+ }
+ }
+
+ private fun bindHomepage(addon: Addon) {
+ if (addon.homepageUrl.isBlank()) {
+ binding.homePageLabel.isVisible = false
+ binding.homePageDivider.isVisible = false
+ return
+ }
+
+ binding.homePageLabel.addUnderline()
+ binding.homePageLabel.setOnClickListener {
+ interactor.openWebsite(addon.homepageUrl.toUri())
+ }
+ }
+
+ private fun bindLastUpdated(addon: Addon) {
+ if (addon.updatedAt.isBlank()) {
+ binding.lastUpdatedLabel.isVisible = false
+ binding.lastUpdatedText.isVisible = false
+ binding.lastUpdatedDivider.isVisible = false
+ return
+ }
+
+ val formattedDate = dateFormatter.format(addon.updatedAtDate)
+ binding.lastUpdatedText.text = formattedDate
+ binding.lastUpdatedLabel.joinContentDescriptions(formattedDate)
+ }
+
+ private fun bindVersion(addon: Addon) {
+ var version = addon.installedState?.version
+ if (version.isNullOrEmpty()) {
+ version = addon.version
+ }
+ binding.versionText.text = version
+
+ if (addon.isInstalled()) {
+ binding.versionText.setOnLongClickListener {
+ interactor.showUpdaterDialog(addon)
+ true
+ }
+ } else {
+ binding.versionText.setOnLongClickListener(null)
+ }
+ binding.versionLabel.joinContentDescriptions(version)
+ }
+
+ private fun bindAuthor(addon: Addon) {
+ val author = addon.author
+ if (author == null || author.name.isBlank()) {
+ binding.authorLabel.isVisible = false
+ binding.authorText.isVisible = false
+ binding.authorDivider.isVisible = false
+ return
+ }
+
+ binding.authorText.text = author.name
+
+ if (author.url.isNotBlank()) {
+ binding.authorText.setTextColor(binding.root.context.getColorFromAttr(R.attr.textAccent))
+ binding.authorText.addUnderline()
+ binding.authorText.setOnClickListener {
+ interactor.openWebsite(author.url.toUri())
+ }
+ }
+ binding.authorLabel.joinContentDescriptions(author.name)
+ }
+
+ private fun bindDetails(addon: Addon) {
+ val detailsText = addon.translateDescription(binding.root.context)
+
+ val parsedText = detailsText.replace("\n", " ")
+ val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
+
+ val spannableStringBuilder = SpannableStringBuilder(text)
+ val links = spannableStringBuilder.getSpans()
+ for (link in links) {
+ addActionToLinks(spannableStringBuilder, link)
+ }
+ binding.details.text = spannableStringBuilder
+ binding.details.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ private fun addActionToLinks(
+ spannableStringBuilder: SpannableStringBuilder,
+ link: URLSpan,
+ ) {
+ val start = spannableStringBuilder.getSpanStart(link)
+ val end = spannableStringBuilder.getSpanEnd(link)
+ val flags = spannableStringBuilder.getSpanFlags(link)
+ val clickable: ClickableSpan = object : ClickableSpan() {
+ override fun onClick(view: View) {
+ view.setOnClickListener {
+ interactor.openWebsite(link.url.toUri())
+ }
+ }
+ }
+ spannableStringBuilder.setSpan(clickable, start, end, flags)
+ spannableStringBuilder.removeSpan(link)
+ }
+
+ private fun bindDetailUrl(addon: Addon) {
+ if (addon.detailUrl.isBlank()) {
+ binding.detailUrl.isVisible = false
+ binding.detailUrlDivider.isVisible = false
+ return
+ }
+
+ binding.detailUrl.addUnderline()
+ binding.detailUrl.setOnClickListener {
+ interactor.openWebsite(addon.detailUrl.toUri())
+ }
+ }
+
+ @VisibleForTesting
+ internal fun TextView.joinContentDescriptions(text: String) {
+ this.contentDescription = "${this.text} $text"
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt
new file mode 100644
index 0000000000..d6f9673981
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonDetailsFragment.kt
@@ -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.addons
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.navArgs
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.showInformationDialog
+import mozilla.components.feature.addons.ui.translateName
+import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateAttemptStorage
+import org.mozilla.fenix.BrowserDirection
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.FragmentAddOnDetailsBinding
+import org.mozilla.fenix.ext.showToolbar
+
+/**
+ * A fragment to show the details of an add-on.
+ */
+class AddonDetailsFragment : Fragment(R.layout.fragment_add_on_details), AddonDetailsInteractor {
+
+ private val updateAttemptStorage by lazy { UpdateAttemptStorage(requireContext()) }
+ private val args by navArgs()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val binding = FragmentAddOnDetailsBinding.bind(view)
+ AddonDetailsBindingDelegate(binding, interactor = this).bind(args.addon)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ context?.let {
+ showToolbar(title = args.addon.translateName(it))
+ }
+ }
+
+ override fun openWebsite(url: Uri) {
+ (activity as HomeActivity).openToBrowserAndLoad(
+ searchTermOrURL = url.toString(),
+ newTab = true,
+ from = BrowserDirection.FromAddonDetailsFragment,
+ )
+ }
+
+ override fun showUpdaterDialog(addon: Addon) {
+ viewLifecycleOwner.lifecycleScope.launch(Main) {
+ val updateAttempt = withContext(IO) {
+ updateAttemptStorage.findUpdateAttemptBy(addon.id)
+ }
+ updateAttempt?.showInformationDialog(requireContext())
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt
new file mode 100644
index 0000000000..8c08a95fd7
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonInternalSettingsFragment.kt
@@ -0,0 +1,51 @@
+/* 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.addons
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import mozilla.components.feature.addons.ui.translateName
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.FragmentAddOnInternalSettingsBinding
+import org.mozilla.fenix.ext.showToolbar
+
+/**
+ * A fragment to show the internal settings of an add-on.
+ */
+class AddonInternalSettingsFragment : AddonPopupBaseFragment() {
+
+ private val args by navArgs()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ initializeSession()
+ return inflater.inflate(R.layout.fragment_add_on_internal_settings, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val binding = FragmentAddOnInternalSettingsBinding.bind(view)
+ args.addon.installedState?.optionsPageUrl?.let {
+ engineSession?.let { engineSession ->
+ binding.addonSettingsEngineView.render(engineSession)
+ engineSession.loadUrl(it)
+ }
+ } ?: findNavController().navigateUp()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ context?.let {
+ showToolbar(title = args.addon.translateName(it))
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionDetailsBindingDelegate.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionDetailsBindingDelegate.kt
new file mode 100644
index 0000000000..a1d72cd698
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionDetailsBindingDelegate.kt
@@ -0,0 +1,60 @@
+/* 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.addons
+
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.recyclerview.widget.LinearLayoutManager
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.AddonPermissionsAdapter
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.FragmentAddOnPermissionsBinding
+import org.mozilla.fenix.theme.ThemeManager
+
+interface AddonPermissionsDetailsInteractor {
+
+ /**
+ * Open the given siteUrl in the browser.
+ */
+ fun openWebsite(addonSiteUrl: Uri)
+}
+
+/**
+ * Shows the permission details of an add-on.
+ */
+class AddonPermissionDetailsBindingDelegate(
+ val binding: FragmentAddOnPermissionsBinding,
+ private val interactor: AddonPermissionsDetailsInteractor,
+) {
+
+ fun bind(addon: Addon) {
+ bindPermissions(addon)
+ bindLearnMore()
+ }
+
+ private fun bindPermissions(addon: Addon) {
+ binding.addOnsPermissions.apply {
+ layoutManager = LinearLayoutManager(context)
+ val sortedPermissions = addon.translatePermissions(context).sorted()
+ adapter = AddonPermissionsAdapter(
+ sortedPermissions,
+ style = AddonPermissionsAdapter.Style(
+ ThemeManager.resolveAttribute(R.attr.textPrimary, context),
+ ),
+ )
+ }
+ }
+
+ private fun bindLearnMore() {
+ binding.learnMoreLabel.setOnClickListener {
+ interactor.openWebsite(LEARN_MORE_URL.toUri())
+ }
+ }
+
+ private companion object {
+ const val LEARN_MORE_URL =
+ "https://support.mozilla.org/kb/permission-request-messages-firefox-extensions"
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionsDetailsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionsDetailsFragment.kt
new file mode 100644
index 0000000000..8c2a0773ad
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPermissionsDetailsFragment.kt
@@ -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.addons
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
+import mozilla.components.feature.addons.ui.translateName
+import org.mozilla.fenix.BrowserDirection
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.R
+import org.mozilla.fenix.databinding.FragmentAddOnPermissionsBinding
+import org.mozilla.fenix.ext.showToolbar
+
+/**
+ * A fragment to show the permissions of an add-on.
+ */
+class AddonPermissionsDetailsFragment :
+ Fragment(R.layout.fragment_add_on_permissions),
+ AddonPermissionsDetailsInteractor {
+
+ private val args by navArgs()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val binding = FragmentAddOnPermissionsBinding.bind(view)
+ AddonPermissionDetailsBindingDelegate(binding, interactor = this).bind(args.addon)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ context?.let {
+ showToolbar(title = args.addon.translateName(it))
+ }
+ }
+
+ override fun openWebsite(addonSiteUrl: Uri) {
+ (activity as HomeActivity).openToBrowserAndLoad(
+ searchTermOrURL = addonSiteUrl.toString(),
+ newTab = true,
+ from = BrowserDirection.FromAddonPermissionsDetailsFragment,
+ )
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt
new file mode 100644
index 0000000000..b1c54ee1f0
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt
@@ -0,0 +1,130 @@
+/* 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.addons
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.engine.window.WindowRequest
+import mozilla.components.feature.prompts.PromptFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+import org.mozilla.fenix.ext.requireComponents
+
+/**
+ * Provides shared functionality to our fragments for add-on settings and
+ * browser/page action popups.
+ */
+abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, UserInteractionHandler {
+ private val promptsFeature = ViewBoundFeatureWrapper()
+
+ protected var session: SessionState? = null
+ protected var engineSession: EngineSession? = null
+ private var canGoBack: Boolean = false
+
+ @Suppress("DEPRECATION")
+ // https://github.com/mozilla-mobile/fenix/issues/19920
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ session?.let {
+ promptsFeature.set(
+ feature = PromptFeature(
+ fragment = this,
+ store = requireComponents.core.store,
+ customTabId = it.id,
+ fragmentManager = parentFragmentManager,
+ fileUploadsDirCleaner = requireComponents.core.fileUploadsDirCleaner,
+ onNeedToRequestPermissions = { permissions ->
+ requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
+ },
+ tabsUseCases = requireComponents.useCases.tabsUseCases,
+ ),
+ owner = this,
+ view = view,
+ )
+ }
+ }
+
+ override fun onDestroyView() {
+ engineSession?.close()
+ session?.let {
+ requireComponents.core.store.dispatch(CustomTabListAction.RemoveCustomTabAction(it.id))
+ }
+ super.onDestroyView()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ engineSession?.register(this)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ engineSession?.unregister(this)
+ }
+
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ session?.let { session ->
+ requireComponents.core.store.dispatch(
+ ContentAction.UpdatePromptRequestAction(
+ session.id,
+ promptRequest,
+ ),
+ )
+ }
+ }
+
+ override fun onWindowRequest(windowRequest: WindowRequest) {
+ if (windowRequest.type == WindowRequest.Type.CLOSE) {
+ findNavController().popBackStack()
+ } else {
+ engineSession?.loadUrl(windowRequest.url)
+ }
+ }
+
+ override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) {
+ canGoBack?.let { this.canGoBack = canGoBack }
+ }
+
+ override fun onBackPressed(): Boolean {
+ return if (this.canGoBack) {
+ engineSession?.goBack()
+ true
+ } else {
+ false
+ }
+ }
+
+ protected fun initializeSession(fromEngineSession: EngineSession? = null) {
+ engineSession = fromEngineSession ?: requireComponents.core.engine.createSession()
+ session = createCustomTab(
+ url = "",
+ source = SessionState.Source.Internal.CustomTab,
+ ).copy(engineState = EngineState(engineSession))
+ requireComponents.core.store.dispatch(CustomTabListAction.AddCustomTabAction(session as CustomTabSessionState))
+ }
+
+ final override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ ) {
+ when (requestCode) {
+ REQUEST_CODE_PROMPT_PERMISSIONS -> promptsFeature.get()?.onPermissionsResult(permissions, grantResults)
+ }
+ }
+
+ companion object {
+ private const val REQUEST_CODE_PROMPT_PERMISSIONS = 1
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt
new file mode 100644
index 0000000000..5eeeeedb3e
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt
@@ -0,0 +1,274 @@
+/* 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.addons
+
+import android.content.Context
+import android.graphics.Typeface
+import android.graphics.fonts.FontStyle.FONT_WEIGHT_MEDIUM
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.annotation.VisibleForTesting
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.launch
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.AddonManager
+import mozilla.components.feature.addons.AddonManagerException
+import mozilla.components.feature.addons.ui.AddonsManagerAdapter
+import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
+import org.mozilla.fenix.BrowserDirection
+import org.mozilla.fenix.BuildConfig
+import org.mozilla.fenix.Config
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.R
+import org.mozilla.fenix.components.FenixSnackbar
+import org.mozilla.fenix.databinding.FragmentAddOnsManagementBinding
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.requireComponents
+import org.mozilla.fenix.ext.runIfFragmentIsAttached
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.ext.showToolbar
+import org.mozilla.fenix.settings.SupportUtils
+import org.mozilla.fenix.theme.ThemeManager
+
+/**
+ * Fragment use for managing add-ons.
+ */
+@Suppress("TooManyFunctions", "LargeClass")
+class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {
+
+ private var binding: FragmentAddOnsManagementBinding? = null
+
+ private var addons: List = emptyList()
+
+ private var adapter: AddonsManagerAdapter? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding = FragmentAddOnsManagementBinding.bind(view)
+ bindRecyclerView()
+ (activity as HomeActivity).webExtensionPromptFeature.onAddonChanged = {
+ runIfFragmentIsAttached {
+ adapter?.updateAddon(it)
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ showToolbar(getString(R.string.preferences_extensions))
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ // letting go of the resources to avoid memory leak.
+ adapter = null
+ binding = null
+ (activity as HomeActivity).webExtensionPromptFeature.onAddonChanged = {}
+ }
+
+ private fun bindRecyclerView() {
+ val managementView = AddonsManagementView(
+ navController = findNavController(),
+ onInstallButtonClicked = ::installAddon,
+ onMoreAddonsButtonClicked = ::openAMO,
+ onLearnMoreClicked = ::openLearnMoreLink,
+ )
+
+ val recyclerView = binding?.addOnsList
+ recyclerView?.layoutManager = LinearLayoutManager(requireContext())
+ val shouldRefresh = adapter != null
+
+ lifecycleScope.launch(IO) {
+ try {
+ addons = requireContext().components.addonManager.getAddons()
+ // Add-ons that should be excluded in Mozilla Online builds
+ val excludedAddonIDs = if (Config.channel.isMozillaOnline &&
+ !BuildConfig.MOZILLA_ONLINE_ADDON_EXCLUSIONS.isNullOrEmpty()
+ ) {
+ BuildConfig.MOZILLA_ONLINE_ADDON_EXCLUSIONS.toList()
+ } else {
+ emptyList()
+ }
+ lifecycleScope.launch(Dispatchers.Main) {
+ runIfFragmentIsAttached {
+ if (!shouldRefresh) {
+ adapter = AddonsManagerAdapter(
+ addonsManagerDelegate = managementView,
+ addons = addons,
+ style = createAddonStyle(requireContext()),
+ excludedAddonIDs = excludedAddonIDs,
+ store = requireComponents.core.store,
+ )
+ }
+ binding?.addOnsProgressBar?.isVisible = false
+ binding?.addOnsEmptyMessage?.isVisible = false
+
+ recyclerView?.adapter = adapter
+ recyclerView?.accessibilityDelegate = object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+
+ adapter?.let {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ info.collectionInfo = AccessibilityNodeInfo.CollectionInfo(
+ it.itemCount,
+ 1,
+ false,
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ info.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
+ it.itemCount,
+ 1,
+ false,
+ )
+ }
+ }
+ }
+ }
+
+ if (shouldRefresh) {
+ adapter?.updateAddons(addons)
+ }
+ }
+ }
+ } catch (e: AddonManagerException) {
+ lifecycleScope.launch(Dispatchers.Main) {
+ runIfFragmentIsAttached {
+ binding?.let {
+ showSnackBar(
+ it.root,
+ getString(R.string.mozac_feature_addons_failed_to_query_extensions),
+ )
+ }
+ binding?.addOnsProgressBar?.isVisible = false
+ binding?.addOnsEmptyMessage?.isVisible = true
+ }
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun showErrorSnackBar(text: String, anchorView: View? = this.view) {
+ runIfFragmentIsAttached {
+ anchorView?.let {
+ showSnackBar(it, text, FenixSnackbar.LENGTH_LONG)
+ }
+ }
+ }
+
+ private fun createAddonStyle(context: Context): AddonsManagerAdapter.Style {
+ val sectionsTypeFace = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Typeface.create(Typeface.DEFAULT, FONT_WEIGHT_MEDIUM, false)
+ } else {
+ Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
+ }
+
+ return AddonsManagerAdapter.Style(
+ sectionsTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context),
+ addonNameTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context),
+ addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.textSecondary, context),
+ sectionsTypeFace = sectionsTypeFace,
+ addonAllowPrivateBrowsingLabelDrawableRes = R.drawable.ic_add_on_private_browsing_label,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun provideAddonManger(): AddonManager {
+ return requireContext().components.addonManager
+ }
+
+ internal fun provideAccessibilityServicesEnabled(): Boolean {
+ return requireContext().settings().accessibilityServicesEnabled
+ }
+
+ internal fun installAddon(addon: Addon) {
+ binding?.addonProgressOverlay?.overlayCardView?.visibility = View.VISIBLE
+ if (provideAccessibilityServicesEnabled()) {
+ binding?.let { announceForAccessibility(it.addonProgressOverlay.addOnsOverlayText.text) }
+ }
+ val installOperation = provideAddonManger().installAddon(
+ url = addon.downloadUrl,
+ installationMethod = InstallationMethod.MANAGER,
+ onSuccess = {
+ runIfFragmentIsAttached {
+ adapter?.updateAddon(it)
+ binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
+ }
+ },
+ onError = { _ ->
+ binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
+ },
+ )
+ binding?.addonProgressOverlay?.cancelButton?.setOnClickListener {
+ lifecycleScope.launch(Dispatchers.Main) {
+ val safeBinding = binding
+ // Hide the installation progress overlay once cancellation is successful.
+ if (installOperation.cancel().await()) {
+ safeBinding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
+ }
+ }
+ }
+ }
+
+ private fun announceForAccessibility(announcementText: CharSequence) {
+ val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+ } else {
+ @Suppress("DEPRECATION")
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+ }
+
+ binding?.addonProgressOverlay?.overlayCardView?.onInitializeAccessibilityEvent(event)
+ event.text.add(announcementText)
+ event.contentDescription = null
+ binding?.addonProgressOverlay?.overlayCardView?.let {
+ it.parent?.requestSendAccessibilityEvent(
+ it,
+ event,
+ )
+ }
+ }
+
+ private fun openAMO() {
+ openLinkInNewTab(AMO_HOMEPAGE_FOR_ANDROID)
+ }
+
+ private fun openLearnMoreLink(link: AddonsManagerAdapterDelegate.LearnMoreLinks, addon: Addon) {
+ val url = when (link) {
+ AddonsManagerAdapterDelegate.LearnMoreLinks.BLOCKLISTED_ADDON ->
+ "${BuildConfig.AMO_BASE_URL}/android/blocked-addon/${addon.id}/"
+ AddonsManagerAdapterDelegate.LearnMoreLinks.ADDON_NOT_CORRECTLY_SIGNED ->
+ SupportUtils.getSumoURLForTopic(requireContext(), SupportUtils.SumoTopic.UNSIGNED_ADDONS)
+ }
+ openLinkInNewTab(url)
+ }
+
+ private fun openLinkInNewTab(url: String) {
+ (activity as HomeActivity).openToBrowserAndLoad(
+ searchTermOrURL = url,
+ newTab = true,
+ from = BrowserDirection.FromAddonsManagementFragment,
+ )
+ }
+
+ companion object {
+ // This is locale-less on purpose so that the content negotiation happens on the AMO side because the current
+ // user language might not be supported by AMO and/or the language might not be exactly what AMO is expecting
+ // (e.g. `en` instead of `en-US`).
+ private const val AMO_HOMEPAGE_FOR_ANDROID = "${BuildConfig.AMO_BASE_URL}/android/"
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt
new file mode 100644
index 0000000000..54da444d24
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.addons
+
+import androidx.navigation.NavController
+import mozilla.components.feature.addons.Addon
+import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.navigateSafe
+
+/**
+ * View used for managing add-ons.
+ */
+class AddonsManagementView(
+ private val navController: NavController,
+ private val onInstallButtonClicked: (Addon) -> Unit,
+ private val onMoreAddonsButtonClicked: () -> Unit,
+ private val onLearnMoreClicked: (link: AddonsManagerAdapterDelegate.LearnMoreLinks, addon: Addon) -> Unit,
+) : AddonsManagerAdapterDelegate {
+
+ override fun onAddonItemClicked(addon: Addon) {
+ if (addon.isInstalled()) {
+ showInstalledAddonDetailsFragment(addon)
+ } else {
+ showDetailsFragment(addon)
+ }
+ }
+
+ override fun onInstallAddonButtonClicked(addon: Addon) {
+ onInstallButtonClicked(addon)
+ }
+
+ override fun onNotYetSupportedSectionClicked(unsupportedAddons: List) {
+ showNotYetSupportedAddonFragment(unsupportedAddons)
+ }
+
+ override fun shouldShowFindMoreAddonsButton(): Boolean = true
+
+ override fun onFindMoreAddonsButtonClicked() {
+ onMoreAddonsButtonClicked()
+ }
+
+ override fun onLearnMoreLinkClicked(link: AddonsManagerAdapterDelegate.LearnMoreLinks, addon: Addon) {
+ onLearnMoreClicked(link, addon)
+ }
+
+ private fun showInstalledAddonDetailsFragment(addon: Addon) {
+ val directions =
+ AddonsManagementFragmentDirections.actionAddonsManagementFragmentToInstalledAddonDetails(
+ addon,
+ )
+ navController.navigateSafe(R.id.addonsManagementFragment, directions)
+ }
+
+ private fun showDetailsFragment(addon: Addon) {
+ val directions =
+ AddonsManagementFragmentDirections.actionAddonsManagementFragmentToAddonDetailsFragment(
+ addon,
+ )
+ navController.navigateSafe(R.id.addonsManagementFragment, directions)
+ }
+
+ private fun showNotYetSupportedAddonFragment(unsupportedAddons: List) {
+ val directions =
+ AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
+ unsupportedAddons.toTypedArray(),
+ )
+ navController.navigate(directions)
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt
new file mode 100644
index 0000000000..05da48e75d
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt
@@ -0,0 +1,25 @@
+/* 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.addons
+
+import android.view.View
+import org.mozilla.fenix.components.FenixSnackbar
+
+/**
+ * Shows the Fenix Snackbar in the given view along with the provided text.
+ *
+ * @param view A [View] used to determine a parent for the [FenixSnackbar].
+ * @param text The text to display in the [FenixSnackbar].
+ * @param duration The duration to show the [FenixSnackbar] for.
+ */
+internal fun showSnackBar(view: View, text: String, duration: Int = FenixSnackbar.LENGTH_SHORT) {
+ FenixSnackbar.make(
+ view = view,
+ duration = duration,
+ isDisplayedWithBrowserToolbar = true,
+ )
+ .setText(text)
+ .show()
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundController.kt
new file mode 100644
index 0000000000..d3cf9234f6
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundController.kt
@@ -0,0 +1,49 @@
+/* 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.addons
+
+import android.os.Handler
+import android.os.Looper
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptObserver
+import org.mozilla.fenix.components.AppStore
+import kotlin.system.exitProcess
+
+/**
+ * Controller for handling extensions process spawning disabled events. This is for when the app is
+ * in background, the app is killed to prevent extensions from being disabled and network requests
+ * continuing.
+ *
+ * @param browserStore The [BrowserStore] which holds the state for showing the dialog.
+ * @param appStore The [AppStore] containing the application state.
+ * @param onExtensionsProcessDisabled Invoked when the app is in background and extensions process
+ * is disabled.
+ */
+class ExtensionsProcessDisabledBackgroundController(
+ browserStore: BrowserStore,
+ appStore: AppStore,
+ onExtensionsProcessDisabled: () -> Unit = { killApp() },
+) : ExtensionsProcessDisabledPromptObserver(
+ store = browserStore,
+ shouldCancelOnStop = false,
+ onShowExtensionsProcessDisabledPrompt = {
+ if (!appStore.state.isForeground) {
+ onExtensionsProcessDisabled()
+ }
+ },
+) {
+
+ companion object {
+ /**
+ * When a dialog can't be shown because the app is in the background, instead the app will
+ * be killed to prevent leaking network data without extensions enabled.
+ */
+ private fun killApp() {
+ Handler(Looper.getMainLooper()).post {
+ exitProcess(0)
+ }
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundController.kt
new file mode 100644
index 0000000000..22aa0a4d33
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundController.kt
@@ -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.addons
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.widget.Button
+import android.widget.TextView
+import androidx.annotation.UiContext
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.browser.state.action.ExtensionsProcessAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.ktx.android.content.appName
+import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptObserver
+import org.mozilla.fenix.R
+import org.mozilla.fenix.components.AppStore
+import org.mozilla.fenix.ext.components
+
+/**
+ * Controller for handling extensions process spawning disabled events. When the app is in
+ * foreground this will call for a dialog to decide on correct action to take (retry enabling
+ * process spawning or disable extensions).
+ *
+ * @param context to show the AlertDialog
+ * @param browserStore The [BrowserStore] which holds the state for showing the dialog
+ * @param appStore The [AppStore] containing the application state
+ * @param builder to use for creating the dialog which can be styled as needed
+ * @param appName to be added to the message. Optional and mainly relevant for testing
+ */
+class ExtensionsProcessDisabledForegroundController(
+ @UiContext context: Context,
+ browserStore: BrowserStore = context.components.core.store,
+ appStore: AppStore = context.components.appStore,
+ builder: AlertDialog.Builder = AlertDialog.Builder(context),
+ appName: String = context.appName,
+) : ExtensionsProcessDisabledPromptObserver(
+ store = browserStore,
+ shouldCancelOnStop = true,
+ {
+ if (appStore.state.isForeground) {
+ presentDialog(context, browserStore, builder, appName)
+ }
+ },
+) {
+ override fun onDestroy(owner: LifecycleOwner) {
+ super.onDestroy(owner)
+ // In case the activity gets destroyed, we want to re-create the dialog.
+ shouldCreateDialog = true
+ }
+
+ companion object {
+ private var shouldCreateDialog: Boolean = true
+
+ /**
+ * Present a dialog to the user notifying of extensions process spawning disabled and also asking
+ * whether they would like to continue trying or disable extensions. If the user chooses to retry,
+ * enable the extensions process spawning. Otherwise, disable it.
+ *
+ * @param context to show the AlertDialog
+ * @param store The [BrowserStore] which holds the state for showing the dialog
+ * @param builder to use for creating the dialog which can be styled as needed
+ * @param appName to be added to the message. Necessary to be added as a param for testing
+ */
+ private fun presentDialog(
+ @UiContext context: Context,
+ store: BrowserStore,
+ builder: AlertDialog.Builder,
+ appName: String,
+ ) {
+ if (!shouldCreateDialog) {
+ return
+ }
+
+ val message = context.getString(R.string.extension_process_crash_dialog_message, appName)
+ var onDismissDialog: (() -> Unit)? = null
+ val layout = LayoutInflater.from(context)
+ .inflate(R.layout.crash_extension_dialog, null, false)
+ layout?.apply {
+ findViewById(R.id.message)?.text = message
+ findViewById