Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Appium test to install app from Play store with Lantern running #906

Merged
merged 20 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
92 changes: 92 additions & 0 deletions appium_kotlin/app/src/test/java/appium_kotlin/tests/AppTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -63,6 +73,8 @@ class AppTest() : BaseTest() {
// Report and issue flow
reportAnIssueFlow(androidDriver, taskId, flutterFinder)

googlePlayFlow(androidDriver, taskId, flutterFinder)

if (!isLocalRun) {
testPassed(androidDriver)
}
Expand Down Expand Up @@ -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))
}

}
82 changes: 45 additions & 37 deletions appium_kotlin/app/src/test/java/appium_kotlin/tests/BaseTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 */
Expand All @@ -31,7 +29,6 @@ open class BaseTest {
lateinit var config: JsonObject
lateinit var service: AppiumDriverLocalService


@JvmStatic
@MethodSource("devices")
fun devices(): Stream<Int> {
Expand All @@ -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()
Expand All @@ -103,56 +81,87 @@ 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")

// Set capabilities for the specific environment
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")
println("TaskId: $taskId | Car $capabilities")
return driver
}


fun testPassed(driver: AndroidDriver) {
val jse = (driver as JavascriptExecutor)
jse.executeScript("browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\": \"passed\", \"reason\": \"All test passed!\"}}")
}

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 {
Expand All @@ -166,5 +175,4 @@ open class BaseTest {
protected fun print(tag: String, message: String) {
println("[$tag] $message")
}

}
}
Loading