Appium Example

Link to Appium Example copied to clipboard
tip

We're developing an Appium Plugin that's set to launch early 2024. This plugin will enable you to better integrate accessibility testing throughout your Appium tests, no matter the language they are written in!

Using Appium with axe DevTools Today? The below information is provided in context of implementing the axeDevToolsUIKit framework (setup available to view here) and engaging with an embedded floating action button to kick off a scan. The axeDevToolsUIKit framework is decommissioned and will not receive new updates while the plugin is being built.

Supported within:
axeDevTools UIKit framework

important

Before running your tests in Appium, ensure the application under test is set up to run axe DevTools Mobile by embedding the framework into your application. Resource: Setup Guide for Embedding the framework

Next, replace the call to axe?.showA11yFAB() with axe?.showA11yFAB(customFAB: AutomatedTestFAB()). This provides an invisible floating action button for tests to engage with that won't interfere with any UI assertions. Resource: UITesting with axeDevToolsUIKit

Introduction

By embedding the framework into your application, Appium can initiate a scan by engaging with the floating action button by its accessibility identifier: com.deque.axeDevTools.accessibilityFab. Engage with the floating action button at any point in your tests you'd like to scan for accessibility issues.

Note: Appium support for accessibility testing is limited to UIKit implementations currently. Support for other languages is coming soon.

Examples

Python Client

Resource: Appium Python Client Documentation

Follow the comments marked with TODO to update your specific configurations to start testing with the script as is. Grab a Deque API Key from your axe Account Settings page.

from appium import webdriver
import os

import requests
from appium.webdriver import WebElement
from appium.webdriver.common.appiumby import AppiumBy

## TODO: Update your configurations
app = "com.dequesystems.axe-devtools-ios-sample-app"
udid = "DFB0EB3F-2C3D-4F62-B684-39F3D12F0DCD"
platformVersion = "16.2"
deviceName = "iPhone 14"
dequeAPIKey = ""


## Class Definitions for parsing the result from the server:
class AxeRuleResult:
    def __init__(self, ruleId, ruleSummary, axeViewId, status, impact, props, isVisibleToUser=True):
        self.ruleId = ruleId
        self.ruleSummary = ruleSummary
        self.axeViewId = axeViewId
        self.status = status
        self.impact = impact
        self.props = props
        self.isVisibleToUser = isVisibleToUser
class AxeResult:
    def __init__(self, axeRuleResults, userName, scanName, tags):
        self.axeRuleResults = axeRuleResults
        self.userName = userName
        self.scanName = scanName
        self.tags = tags


class DemoWithAPIRequest:
    def __init__(self):
        self.driver = None
        self.client = requests.Session()

    def setup(self):
        success = True
        desired_caps = {}
        desired_caps['platformName'] = 'iOS'
        desired_caps['platformVersion'] = platformVersion
        desired_caps['deviceName'] = deviceName
        desired_caps['udid'] = udid
        desired_caps['automationName'] = 'XCUITest'
        desired_caps['realDeviceLogger'] = 'idevicesyslog'
        desired_caps['app'] = app
        ## TODO: UPDATE URL for your appium server:
        self.driver = webdriver.Remote('http://127.0.0.1:4723', desired_caps)


    def runA11y(self):
        # Navigate and Click the FAB
        fab = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='com.deque.axeDevTools.accessibilityFab')
        fab.click()

        # Wait for network request to finish and FAB to return.
        self.driver.implicitly_wait(10)

        # Getting Result Key from the FAB
        resultKey = fab.get_attribute("label")

        # Fetch Rule Results from API
        url = "https://axe-mobile-backend.deque.com/attest/result/axe/"+resultKey
        headers = {
            "X-Api-Key": dequeAPIKey,
            "Content-Type": "application/json"
        }
        try:
            response = self.client.get(url, headers=headers)
            if response.status_code == 200:
                content = response.json()

                # Parse JSON and output the result
                axeResult = AxeResult(
                    axeRuleResults=[AxeRuleResult(**ruleResult) for ruleResult in content['axeRuleResults']],
                    userName=content['userName'],
                    scanName=content['scanName'],
                    tags=content['tags']
                )

                for rule in axeResult.axeRuleResults:
                    if rule.status == "FAIL":
                        print(f"rule: {rule.ruleId}")
            else:
                print(f"httpCode: {response.status_code}")
        except Exception as e:
            print(f"error: {str(e)}")


demo = DemoWithAPIRequest()
demo.setup()
demo.runA11y()

Java Client

Resource: Appium Java Client Documentation

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

import com.google.gson.Gson
import io.appium.java_client.AppiumBy
import io.appium.java_client.ios.IOSDriver
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.BufferedReader
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: IOSDriver
    val appFilePath = "path/to/your/application.app"

    @Before
    fun setup() {
        val capabilities = DesiredCapabilities()

        capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone 14")
        capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS)
        capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "16.0")
        capabilities.setCapability(MobileCapabilityType.APP, File(appFilePath).absolutePath)

        driver = IOSDriver(URL("http://127.0.0.1:4723/wd/hub"), capabilities)
    }

    @Test
    fun runA11y() {
        // Navigate and Click the FAB
        driver.findElement(
            AppiumBy.accessibilityId("com.deque.axeDevTools.accessibilityFab")
        ).click()

        val webDriverWait = WebDriverWait(driver, Duration.ofSeconds(10))
        webDriverWait.until(ExpectedCondition { input: WebDriver? ->
            input?.findElement(AppiumBy.accessibilityId("com.deque.axeDevTools.accessibilityFab"))
            true
        })

        // Getting Result Key from the FAB
        val resultKey: String = driver.findElement(
            AppiumBy.accessibilityId("com.deque.axeDevTools.accessibilityFab")
        ).getAttribute("label")


        // 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>?
)