You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt

434 lines
15 KiB
Kotlin

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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.app.ActivityManager
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.view.View
import androidx.browser.customtabs.CustomTabsIntent
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.AssertionFailedError
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher
import org.junit.Assert
import org.junit.Assert.assertTrue
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
import org.mozilla.fenix.ui.robots.BrowserRobot
import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.gecko.util.ThreadUtils
import java.util.Locale
import java.util.regex.Pattern
object TestHelper {
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
val appName = appContext.appName
var mDevice: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val packageName: String = appContext.packageName
fun scrollToElementByText(text: String): UiScrollable {
val appView = UiScrollable(UiSelector().scrollable(true))
appView.waitForExists(waitingTime)
appView.scrollTextIntoView(text)
return appView
}
fun longTapSelectItem(url: Uri) {
mDevice.waitNotNull(
Until.findObject(By.text(url.toString())),
waitingTime,
)
onView(
allOf(
withId(R.id.url),
withText(url.toString()),
),
).perform(longClick())
}
fun restartApp(activity: HomeActivityIntentTestRule) {
with(activity) {
updateCachedSettings()
finishActivity()
mDevice.waitForIdle()
launchActivity(null)
}
}
fun getPermissionAllowID(): String {
return when
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
true -> "com.android.permissioncontroller"
false -> "com.android.packageinstaller"
}
}
fun waitUntilObjectIsFound(resourceName: String) {
mDevice.waitNotNull(
Until.findObjects(By.res(resourceName)),
waitingTime,
)
}
fun waitUntilSnackbarGone() {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/snackbar_layout"),
).waitUntilGone(waitingTime)
}
fun verifyUrl(urlSubstring: String, resourceName: String, resId: Int) {
waitUntilObjectIsFound(resourceName)
mDevice.findObject(UiSelector().text(urlSubstring)).waitForExists(waitingTime)
onView(withId(resId)).check(ViewAssertions.matches(withText(CoreMatchers.containsString(urlSubstring))))
}
fun openAppFromExternalLink(url: String) {
val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse(url)
`package` = packageName
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(intent)
} catch (ex: ActivityNotFoundException) {
intent.setPackage(null)
context.startActivity(intent)
}
}
// Remove test file from Google Photos (AOSP) on Firebase
fun deleteDownloadFromStorage() {
val deleteButton = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/trash"))
deleteButton.waitForExists(waitingTime)
deleteButton.click()
// Sometimes there's a secondary confirmation
try {
val deleteConfirm = mDevice.findObject(UiSelector().text("Got it"))
deleteConfirm.waitForExists(waitingTime)
deleteConfirm.click()
} catch (e: UiObjectNotFoundException) {
// Do nothing
}
val trashIt = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/move_to_trash"))
trashIt.waitForExists(waitingTime)
trashIt.click()
}
fun setNetworkEnabled(enabled: Boolean) {
val networkDisconnectedIdlingResource = NetworkConnectionIdlingResource(false)
val networkConnectedIdlingResource = NetworkConnectionIdlingResource(true)
when (enabled) {
true -> {
mDevice.executeShellCommand("svc data enable")
mDevice.executeShellCommand("svc wifi enable")
// Wait for network connection to be completely enabled
IdlingRegistry.getInstance().register(networkConnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkConnectedIdlingResource)
}
}
false -> {
mDevice.executeShellCommand("svc data disable")
mDevice.executeShellCommand("svc wifi disable")
// Wait for network connection to be completely disabled
IdlingRegistry.getInstance().register(networkDisconnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkDisconnectedIdlingResource)
}
}
}
}
fun createCustomTabIntent(
pageUrl: String,
customMenuItemLabel: String = "",
customActionButtonDescription: String = "",
): Intent {
val appContext = InstrumentationRegistry.getInstrumentation()
.targetContext
.applicationContext
val pendingIntent = PendingIntent.getActivity(appContext, 0, Intent(), IntentUtils.defaultIntentPendingFlags)
val customTabsIntent = CustomTabsIntent.Builder()
.addMenuItem(customMenuItemLabel, pendingIntent)
.setShareState(CustomTabsIntent.SHARE_STATE_ON)
.setActionButton(
createTestBitmap(),
customActionButtonDescription,
pendingIntent,
true,
)
.build()
customTabsIntent.intent.data = Uri.parse(pageUrl)
return customTabsIntent.intent
}
private fun createTestBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.GREEN)
return bitmap
}
fun isPackageInstalled(packageName: String): Boolean {
return try {
val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager
packageManager.getApplicationInfo(packageName, 0).enabled
} catch (exception: PackageManager.NameNotFoundException) {
false
}
}
fun assertExternalAppOpens(appPackageName: String) {
if (isPackageInstalled(appPackageName)) {
try {
intended(toPackage(appPackageName))
} catch (e: AssertionFailedError) {
e.printStackTrace()
}
} else {
mDevice.waitNotNull(
Until.findObject(By.text("Could not open file")),
waitingTime,
)
}
}
fun assertNativeAppOpens(appPackageName: String, url: String = "") {
if (isPackageInstalled(appPackageName)) {
mDevice.waitForIdle(waitingTimeShort)
assertTrue(
mDevice.findObject(UiSelector().packageName(appPackageName))
.waitForExists(waitingTime),
)
} else {
BrowserRobot().verifyUrl(url)
}
}
fun assertPlayStoreOpens() {
if (isPackageInstalled(Constants.PackageName.GOOGLE_PLAY_SERVICES)) {
try {
intended(toPackage(Constants.PackageName.GOOGLE_PLAY_SERVICES))
} catch (e: AssertionFailedError) {
BrowserRobot().verifyRateOnGooglePlayURL()
}
} else {
BrowserRobot().verifyRateOnGooglePlayURL()
}
}
/**
* Checks whether the latest activity of the application is used for custom tabs or PWAs.
*
* @return Boolean value that helps us know if the current activity supports custom tabs or PWAs.
*/
fun isExternalAppBrowserActivityInCurrentTask(): Boolean {
val activityManager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
mDevice.waitForIdle(waitingTimeShort)
return activityManager.appTasks[0].taskInfo.topActivity!!.className == ExternalAppBrowserActivity::class.java.name
}
/**
* Run test with automatically registering idling resources and cleanup.
*
* @param idlingResources zero or more [IdlingResource] to be used when running [testBlock].
* @param testBlock test code to execute.
*/
fun registerAndCleanupIdlingResources(
vararg idlingResources: IdlingResource,
testBlock: () -> Unit,
) {
idlingResources.forEach {
IdlingRegistry.getInstance().register(it)
}
try {
testBlock()
} finally {
idlingResources.forEach {
IdlingRegistry.getInstance().unregister(it)
}
}
}
// exit from Menus to home screen or browser
fun exitMenu() {
val toolbar =
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
while (!toolbar.waitForExists(waitingTimeShort)) {
mDevice.pressBack()
}
}
fun UiDevice.waitForObjects(obj: UiObject, waitingTime: Long = TestAssetHelper.waitingTime) {
this.waitForIdle()
Assert.assertNotNull(obj.waitForExists(waitingTime))
}
fun hasCousin(matcher: Matcher<View>): Matcher<View> {
return withParent(
hasSibling(
withChild(
matcher,
),
),
)
}
fun getStringResource(id: Int) = appContext.resources.getString(id, appName)
fun setCustomSearchEngine(searchEngine: SearchEngine) {
with(appContext.components.useCases.searchUseCases) {
addSearchEngine(searchEngine)
selectSearchEngine(searchEngine)
}
}
fun grantPermission() {
if (Build.VERSION.SDK_INT >= 23) {
mDevice.findObject(
By.text(
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.R -> Pattern.compile(
"WHILE USING THE APP",
Pattern.CASE_INSENSITIVE,
)
else -> Pattern.compile("Allow", Pattern.CASE_INSENSITIVE)
},
),
).click()
}
}
fun denyPermission() {
if (Build.VERSION.SDK_INT >= 23) {
mDevice.findObject(
By.text(
when (Build.VERSION.SDK_INT) {
Build.VERSION_CODES.R -> Pattern.compile(
"DENY",
Pattern.CASE_INSENSITIVE,
)
else -> Pattern.compile("Deny", Pattern.CASE_INSENSITIVE)
},
),
).click()
}
}
fun isTestLab(): Boolean {
return Settings.System.getString(appContext.contentResolver, "firebase.test.lab").toBoolean()
}
private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun generateRandomString(stringLength: Int) =
(1..stringLength)
.map { kotlin.random.Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
/**
* Changes the default language of the entire device, not just the app.
* Runs the test in its testBlock.
* Cleans up and sets the default locale after it's are done.
*/
fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) {
val defaultLocale = Locale.getDefault()
try {
setSystemLocale(locale)
testBlock()
ThreadUtils.runOnUiThread { testRule.activity.recreate() }
} catch (e: Exception) {
e.printStackTrace()
} finally {
setSystemLocale(defaultLocale)
}
}
/**
* Changes the default language of the entire device, not just the app.
*/
private fun setSystemLocale(locale: Locale) {
val activityManagerNative = Class.forName("android.app.ActivityManagerNative")
val am = activityManagerNative.getMethod("getDefault", *arrayOfNulls(0))
.invoke(activityManagerNative, *arrayOfNulls(0))
val config = InstrumentationRegistry.getInstrumentation().context.resources.configuration
config.javaClass.getDeclaredField("locale")[config] = locale
config.javaClass.getDeclaredField("userSetLocale").setBoolean(config, true)
am.javaClass.getMethod(
"updateConfiguration",
Configuration::class.java,
).invoke(am, config)
}
/**
* Creates clipboard data.
*/
fun setTextToClipBoard(context: Context, message: String) {
val clipBoard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("label", message)
clipBoard.setPrimaryClip(clipData)
}
}