/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.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): Matcher { 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 = ('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, 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) } }