diff --git a/README.md b/README.md index 6953eaa04..f73c3ea4a 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,42 @@ You'd most probably wanna run this against Lantern's staging servers **and** tur - You can debug pro-server-neu's staging instance (i.e., `api-staging.getiantem.org`) using a combination of log, telemetry and checking the staging Redis instance (see [here](https://github.com/getlantern/pro-server-neu/blob/c79c1b8da9e418bc4b075392fde9b051c699141d/README.md?plain=1#L125) for more info) +## Running Appium tests locally + +To run the Appium tests locally with a connected device, you need to follow a few steps: + +1. Install appium with npm: + +```bash +npm install -g appium +``` + +2. Install the necessary drivers: + +```bash +appium driver install uiautomator2 +appium driver install --source=npm appium-flutter-driver +appium driver install espresso +``` + +3. Generate a debug build with `CI=true make android-debug ANDROID_ARCH=all` ... CI needs to be set to true to enable the +Flutter driver extension. + +4. Modify [local_config.json](appium_kotlin/app/src/test/resources/local/local_config.json) to specify the path of a debug build APK on your system, and change `appium:udid` to specify your device ID (you can get this from `adb devices`) + +5. Make sure your device is connected to your computer and then run + +```bash +cd appium_kotlin +./gradlew test +``` + +To run a specific test, you can do + +```bash +./gradlew test --tests '*GooglePlay*' +``` + ## Source Dump Lantern Android source code is made available via source dump tarballs. To create one, run: diff --git a/appium_kotlin/app/src/test/java/appium_kotlin/tests/AppTest.kt b/appium_kotlin/app/src/test/java/appium_kotlin/tests/AppTest.kt index 5140b56b4..b1a19282c 100644 --- a/appium_kotlin/app/src/test/java/appium_kotlin/tests/AppTest.kt +++ b/appium_kotlin/app/src/test/java/appium_kotlin/tests/AppTest.kt @@ -22,26 +22,36 @@ import appium_kotlin.REPORT_DESCRIPTION import appium_kotlin.REPORT_ISSUE_SUCCESS import appium_kotlin.SEND_REPORT import appium_kotlin.SUPPORT +import io.appium.java_client.MobileBy import io.appium.java_client.TouchAction import io.appium.java_client.android.Activity import io.appium.java_client.android.AndroidDriver +import io.appium.java_client.android.nativekey.AndroidKey +import io.appium.java_client.android.nativekey.KeyEvent +import io.appium.java_client.remote.AndroidMobileCapabilityType import io.appium.java_client.touch.WaitOptions import io.appium.java_client.touch.offset.PointOption import org.junit.jupiter.api.Assertions import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.openqa.selenium.By +import org.openqa.selenium.remote.DesiredCapabilities import org.openqa.selenium.logging.LogEntries import pro.truongsinh.appium_flutter.FlutterFinder import java.io.IOException +import java.net.URL import java.time.Duration import java.time.LocalDate import java.time.format.DateTimeFormatter +import java.util.concurrent.TimeUnit import java.util.regex.Pattern class AppTest() : BaseTest() { private val isLocalRun = (System.getenv("RUN_ENV") ?: "local") == "local" + private val testAppName = "chromecast" + private val testAppPackage = "com.google.android.apps.chromecast.app" + private val testAppActivity = ".DiscoveryActivity" @ParameterizedTest @MethodSource("devices") @@ -63,6 +73,8 @@ class AppTest() : BaseTest() { // Report and issue flow reportAnIssueFlow(androidDriver, taskId, flutterFinder) + googlePlayFlow(androidDriver, taskId, flutterFinder) + if (!isLocalRun) { testPassed(androidDriver) } @@ -404,4 +416,84 @@ class AppTest() : BaseTest() { return "" } + @Throws(IOException::class, InterruptedException::class) + private fun googlePlayFlow( + driver: AndroidDriver, + taskId: Int, + flutterFinder: FlutterFinder + ) { + turnVPNon(driver, taskId, flutterFinder) + driver.startActivity(Activity("com.android.vending", ".AssetBrowserActivity")) + testEstablishPlaySession(driver) + testGooglePlayFeatures(driver) + installAppFromPlayStore(taskId, driver) + } + + fun turnVPNon( + driver: AndroidDriver, + taskId: Int, + flutterFinder: FlutterFinder, + ) { + Thread.sleep(5000) + + switchToContext(ContextType.NATIVE_APP, driver) + Thread.sleep(5000) + driver.activateApp(LANTERN_PACKAGE_ID) + Thread.sleep(5000) + + + switchToContext(ContextType.FLUTTER, driver) + val vpnSwitchFinder = flutterFinder.byType("FlutterSwitch") + vpnSwitchFinder.click() + Thread.sleep(2000) + // Approve VPN Permissions dialog + switchToContext(ContextType.NATIVE_APP, driver) + Thread.sleep(1000) + } + + fun testEstablishPlaySession(driver: AndroidDriver) { + Assertions.assertEquals(driver.currentPackage, "com.android.vending") + Assertions.assertEquals(driver.currentActivity(), ".AssetBrowserActivity") + } + + fun testGooglePlayFeatures(driver: AndroidDriver) { + driver.findElement(By.xpath("//android.widget.FrameLayout[@content-desc = 'Show navigation drawer']"))?.click() + val elements = driver.findElements(By.xpath("//android.widget.TextView")) + if (elements == null) return + for (element in elements) { + if (element.text.equals("Settings")) { + element.click() + break + } + } + } + + fun openSearchForm(driver: AndroidDriver) { + val elements = driver?.findElements(By.xpath("//android.widget.TextView")) + if (elements == null) return + for (element in elements) { + if (element.text.equals("Search for apps & games")) { + element.click() + break + } + } + } + + @Throws(Exception::class) + fun installAppFromPlayStore(taskId: Int, driver: AndroidDriver) { + openSearchForm(driver) + driver.findElement(MobileBy.className("android.widget.EditText"))?.sendKeys(testAppName) + + driver.findElement(By.xpath("//android.support.v7.widget.RecyclerView[1]/android.widget.LinearLayout[1]"))?.click() + + val button = driver.findElement(MobileBy.className("android.widget.Button")) + if (button?.text.equals("Install")) { + println("Installing application") + button?.click() + } + + driver.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS) + driver.pressKey(KeyEvent(AndroidKey.HOME)) + } + } \ No newline at end of file diff --git a/appium_kotlin/app/src/test/java/appium_kotlin/tests/BaseTest.kt b/appium_kotlin/app/src/test/java/appium_kotlin/tests/BaseTest.kt index 9f8c09e40..2fd4f6bf6 100644 --- a/appium_kotlin/app/src/test/java/appium_kotlin/tests/BaseTest.kt +++ b/appium_kotlin/app/src/test/java/appium_kotlin/tests/BaseTest.kt @@ -4,8 +4,8 @@ import appium_kotlin.ContextType import com.google.gson.JsonArray import com.google.gson.JsonObject import com.google.gson.JsonParser -import io.appium.java_client.AppiumDriver import io.appium.java_client.android.AndroidDriver +import io.appium.java_client.remote.MobileCapabilityType import io.appium.java_client.service.local.AppiumDriverLocalService import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException import io.appium.java_client.service.local.AppiumServiceBuilder @@ -16,10 +16,8 @@ import org.junit.jupiter.api.parallel.ExecutionMode import org.junit.jupiter.params.provider.MethodSource import org.openqa.selenium.JavascriptExecutor import org.openqa.selenium.remote.DesiredCapabilities -import pro.truongsinh.appium_flutter.FlutterFinder import java.io.FileReader import java.net.URL -import java.util.concurrent.TimeUnit import java.util.stream.Stream /** Here is the device list-:https://www.browserstack.com/list-of-browsers-and-platforms/app_automate */ @@ -31,7 +29,6 @@ open class BaseTest { lateinit var config: JsonObject lateinit var service: AppiumDriverLocalService - @JvmStatic @MethodSource("devices") fun devices(): Stream { @@ -54,33 +51,14 @@ open class BaseTest { } } - fun setupAndCreateConnection(taskId: Int): AndroidDriver { - println("Setup and creating connection for TaskId: $taskId") + // Initialize DesiredCapabilities + fun initialCapabilities(taskId: Int): DesiredCapabilities { // Initialize DesiredCapabilities val capabilities = DesiredCapabilities() // Get common capabilities from config val commonCapabilities = config["capabilities"] as JsonObject - // Get environment variables if available or get from config - val username = System.getenv("BROWSERSTACK_USERNAME") ?: config.get("username").asString - val accessKey = - System.getenv("BROWSERSTACK_ACCESS_KEY") ?: config.get("access_key").asString val app = System.getenv("BROWSERSTACK_APP_ID") ?: config.get("app").asString - val server = System.getenv("SERVER") ?: config.get("server").asString - // Check if it is a local run - val isLocalRun = (System.getenv("RUN_ENV") ?: "local") == "local" val envs = config["environments"] as JsonArray - // If local run, start Appium Server - if (isLocalRun) { - // Start Appium Server for local run - service = AppiumServiceBuilder() - .withArgument(GeneralServerFlag.ALLOW_INSECURE, "chromedriver_autodownload") - .build() - service.start() - - if (!service.isRunning) { - throw AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!") - } - } // Iterate over common capabilities val it = commonCapabilities.entrySet().iterator() @@ -103,16 +81,15 @@ open class BaseTest { if (capabilities.getCapability(pair.key.toString()) == null) { capabilities.setCapability( pair.key.toString(), - pair.value.toString().replace("\"", "") + pair.value.toString().replace("\"", ""), ) } } } capabilities.setCapability("app", app) -// capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, "Flutter") + capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, "Flutter") println("Setup for TaskId $taskId: $capabilities") - val envCapabilities = envs[taskId] as JsonObject println("TaskId: $taskId | Current Evn $envCapabilities") @@ -120,15 +97,50 @@ open class BaseTest { envCapabilities.entrySet().iterator().forEach { pair -> capabilities.setCapability(pair.key, pair.value.toString().replace("\"", "")) } - val url = if (isLocalRun) { + return capabilities + } + + // Check if it is a local run + fun checkLocalRun(): Boolean { + val isLocalRun = (System.getenv("RUN_ENV") ?: "local") == "local" + // If local run, start Appium Server + if (isLocalRun) { + // Start Appium Server for local run + service = AppiumServiceBuilder() + .withArgument(GeneralServerFlag.ALLOW_INSECURE, "chromedriver_autodownload") + .build() + service.start() + + if (!service.isRunning) { + throw AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!") + } + } + return isLocalRun + } + + fun serviceURL(isLocalRun: Boolean): String { + // Get environment variables if available or get from config + val username = System.getenv("BROWSERSTACK_USERNAME") ?: config.get("username").asString + val accessKey = + System.getenv("BROWSERSTACK_ACCESS_KEY") ?: config.get("access_key").asString + val server = System.getenv("SERVER") ?: config.get("server").asString + return if (isLocalRun) { service.url.toString() } else { - "https://${username}:${accessKey}@$server" + "https://$username:$accessKey@$server" } + } + fun setupAndCreateConnection(taskId: Int): AndroidDriver { + println("Setup and creating connection for TaskId: $taskId") + + val isLocalRun = checkLocalRun() + val capabilities = initialCapabilities(taskId) + + val url = serviceURL(isLocalRun) val driver = AndroidDriver( URL(url), - capabilities + capabilities, ) println("TaskId: $taskId | Driver created") @@ -136,7 +148,6 @@ open class BaseTest { return driver } - fun testPassed(driver: AndroidDriver) { val jse = (driver as JavascriptExecutor) jse.executeScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"passed\", \"reason\": \"All test passed!\"}}") @@ -144,15 +155,13 @@ open class BaseTest { fun testFail(failureMessage: String, driver: AndroidDriver) { val jse = (driver as JavascriptExecutor) - jse.executeScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\":\"failed\", \"reason\": \"$failureMessage\"}}"); + jse.executeScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\":\"failed\", \"reason\": \"$failureMessage\"}}") } - protected fun switchToContext(contextType: ContextType, driver: AndroidDriver) { val context = getContextString(contextType) driver.context(context) print("Android", "Switched to context: $context") - } private fun getContextString(contextType: ContextType): String { @@ -166,5 +175,4 @@ open class BaseTest { protected fun print(tag: String, message: String) { println("[$tag] $message") } - -} \ No newline at end of file +}