Blog Infos
Author
Published
Topics
, , , ,
Published

This document explores Jetpack Compose AndroidView component compatibility with common large-scale E2E testing platforms (Appium + Pytest).

Target Audience: Android developers and E2E test developers

Background

A key challenge when introducing Composables into a View-based codebase is ensuring compatibility with existing tools, particularly E2E testing platforms. A typical setup involves PyTest integrated with Appium WebDriver, which utilizes Selenium WebDriver.

Appium provides AppiumBy to locate and interact with mobile UI elements:

class AppiumBy(By):
    IOS_PREDICATE = '-ios predicate string'
    IOS_CLASS_CHAIN = '-ios class chain'
    ANDROID_UIAUTOMATOR = '-android uiautomator'
    ANDROID_VIEWTAG = '-android viewtag'
    ANDROID_DATA_MATCHER = '-android datamatcher'
    ANDROID_VIEW_MATCHER = '-android viewmatcher'
    ACCESSIBILITY_ID = 'accessibility id'
    IMAGE = '-image'
    CUSTOM = '-custom'

AppiumBy enables element searches using attributes like Accessibility ID, ViewTag, and Class Chain. By setting the same Accessibility ID on both native Android and iOS apps, we would be able to write and execute one test for both platforms.

In Jetpack Compose, contentDescription is crucial for making Composables locatable by Appium:

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = "shared_e2e_accessibility_id"
        )
    }
}

So far, we have made our View-Based component and Composables Locatable by Appium driver whilst keeping it backward compatible.

The Problem

Challenges arise when dealing with hybrid View-Compose components, such as AndroidView or ComposeView. Setting contentDescription on the outer wrapper element does not automatically apply it to the inner element.

This is a more common issue with large codebases where developers use shared components more often. E.g. They’ll have a Composable and setting a contentDescription on it seems to be the only step to pass E2E tests but if that’s an AndroidView internally, in fact this attribute will not be delivered to the very container.

Sometimes we don’t have edit access to the wrapped item as we are consumers of it. So it’s recommended to expose the contentDescription APIs when you share a component.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

No results found.

Working solution

Here is an example of the inner component implementation for the Button Composable and an example of an approach to resolve this issue:

@Composable
fun Button(
    text: String,
    modifier: Modifier = Modifier,
    ...
) {
    AndroidView(modifier = modifier, factory = { context ->
        Button(context = context).apply {
            setText(text)
            textColor?.let { setTextColor(textColor) }
            var accessibilityId: String? = null
            this.foldIn(null as String?) { _, element ->
                if (element is SemanticsModifier) {
                    element.semanticsConfiguration.getOrNull("contentDescription")?.let {
                        accessibilityId = it
                    }
                }
                accessibilityId
            }
            contentDescription = accessibilityId
        }
    })

}

Here, modifier is an exposed parameter, so consumers can set semantics and send it.

On the consumer side, here is how it can be used:

@Composable
fun LoginButtonsSet(
    modifier: Modifier = Modifier,
    ...
) {
    Button(
        text = stringResource(...),
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 24.dp)
            .semantics {
                contentDescription = 'btn_login_signup'
            },
        )
        ....

Once it’s set, on the E2E side, you can use these methods to find the element:

def get_sign_up_button_element(self):
        return find_element(AppiumBy.ACCESSIBILITY_ID, "btn_login_signup")

get_sign_up_button_element().click()
Next Steps

To simplify this process, consider creating extension functions:

  • Modify.getContentDescription: to read contentDescription from any modifier object.
  • Modifier.setContentDescription: to set it on the modifier.

This approach enhances readability and reduces boilerplate code. Here is a simple implementation of it.

utils/AccessibilityModifier.kt

import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsModifier
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.semantics.semantics

/**
 * Key for the contentDescription semantics property.
 * If your composable works with E2E or UI tests,
 * you can use this property to set a unique identifier for the composable.
 */

/**
 * Key for the contentDescription semantics property.
 */
val ContentDescription = SemanticsPropertyKey<String>("ContentDescription")

/**
 * Receiver for the contentDescription semantics property.
 */
var SemanticsPropertyReceiver.contentDescription by ContentDescription

/**
 * Sets the contentDescription of a composable.
 *
 * @param contentDescription The contentDescription to set.
 */
fun Modifier.contentDescription(contentDescription: String): Modifier {
    return semantics {
        this.contentDescription = contentDescription
    }
}

/**
 * Gets the contentDescription of a composable.
 *
 * @return The contentDescription, or null if not set.
 */
fun Modifier.getContentDescription(): String? {
    return this.foldIn(null as String?) { _, element ->
        if (element is SemanticsModifier) {
            element.semanticsConfiguration.getOrNull(ContentDescription)?.let {
                return@foldIn it
            }
        }
    }
}

. . .

 

By carefully considering accessibility and adopting best practices for setting contentDescription within your Jetpack Compose components, you can ensure seamless integration with your existing E2E testing infrastructure. This approach promotes maintainability and simplifies the transition to a Compose-based UI architecture while preserving the reliability of your automated tests.

I hope this exploration provides valuable insights for your Jetpack Compose adoption journey.

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu