Appium with Kotlin

Link to Appium with Kotlin copied to clipboard
tip

Codeless accessibility testing via Appium is Coming Soon!

We're developing an Appium Plugin that's set to launch later this year. This plugin will enable you to better integrate accessibility testing for any mobile application throughout your Appium tests, no matter the language they are written in and without access to source code!

Using Appium with axe DevTools Today?

Today Appium support is only available for native Android XML applications via an embedded code element. Checkout the guide to get started with the embedded code element and come back here once you've hit Step 3.

The following Appium example is written in Kotlin and assumes the use of Appium's java-client. The logic is transferable to other clients.

Setup

build.gradle

plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.5.0'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
    maven { url "https://maven.google.com" }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib"
    implementation "junit:junit:4.13.2"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2"
    
    testImplementation "junit:junit:4.13.1"

    implementation "com.github.appium:java-client:8.1.1"
    implementation "org.seleniumhq.selenium:selenium-java:4.2.1"
    implementation "com.google.code.gson:gson:2.9.0"
    implementation "com.squareup.okhttp3:okhttp:4.10.0"
}

Full Example using REST API

import com.google.gson.Gson
import io.appium.java_client.AppiumBy
import io.appium.java_client.android.AndroidDriver
import io.appium.java_client.remote.AutomationName
import io.appium.java_client.remote.MobileCapabilityType
import io.appium.java_client.remote.MobilePlatform
import okhttp3.OkHttpClient
import okhttp3.Request
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.openqa.selenium.WebDriver
import org.openqa.selenium.remote.DesiredCapabilities
import org.openqa.selenium.support.ui.ExpectedCondition
import org.openqa.selenium.support.ui.WebDriverWait
import java.io.File
import java.net.URL
import java.time.Duration


@RunWith(JUnit4::class)
class DemoWithAPIRequest {
    private val client = OkHttpClient()
    private lateinit var driver: AndroidDriver

    private val DEFAULT_APPIUM_ADDRESS = "http://0.0.0.0:4723/wd/hub"
    private val apkFilePath = "/path/to/app.apk"

    private fun makeDriver(): AndroidDriver {
        val capabilities = DesiredCapabilities()
        capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator")
        capabilities.setCapability(MobileCapabilityType.APP, File(apkFilePath).absolutePath)
        capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID)
        capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2)

        return AndroidDriver(URL(DEFAULT_APPIUM_ADDRESS), capabilities)
    }

    @Before
    fun setup() {
        driver = makeDriver()
    }

    @Test
    fun runA11y() {
        // Navigate and Click the FAB
        driver.findElement(
            AppiumBy.ByAndroidUIAutomator("description(\"Axe\")")
        ).click()

        val webDriverWait = WebDriverWait(driver, Duration.ofSeconds(10))
        webDriverWait.until(ExpectedCondition { input: WebDriver? ->
            input?.findElement(AppiumBy.id("result_key_container"))
            true
        })

        // Getting Result Key from the FAB
        val resultKey = driver.findElement(
            AppiumBy.id("result_key_container")
        ).getAttribute("content-desc")

        // Fetch Rule Results from API
        val apiKey = "your-api-key-here"
        val url = "https://axe-mobile-backend.deque.com/attest/result/axe/$resultKey"
        val request = Request.Builder()
            .url(url)
            .addHeader("X-Api-Key", apiKey)
            .addHeader("Content-Type", "application/json")
            .build()

        try {
            val response = client.newCall(request).execute()
            if (response.code == 200) {
                val content = response.body?.string()

                // Parse JSON and output the result
                val axeResult = Gson().fromJson(content, AxeResult::class.java)

                axeResult.axeRuleResults.forEach {
                    if (it.status == "FAIL") {
                        println("rule: ${it.ruleId}")
                    }
                }
            } else {
                println("httpCode: ${response.code}")
            }
        } catch (t: Throwable) {
            println("error: ${t.message}")
        }
    }
}

data class AxeRuleResult(
    val ruleId: String?,
    val ruleSummary: String?,
    val axeViewId: String?,
    val status: String?, // "PASS", "FAIL", "INCOMPLETE"
    val impact: Int?,
    val props: HashMap<String, Any?>,
    val isVisibileToUser: Boolean = true
)

data class AxeResult(
    val axeRuleResults: List<AxeRuleResult>,
    val userName: String?,
    val scanName: String?,
    val tags: List<String>?
)